Due to debugging reasons (I am hunting down a very weird bug), I need to extend the Laravel classes Illuminate\Database\DatabaseManager, Illuminate\Database\Connection, and Illuminate\Database\Query\Builder.
Hence, I created the following three classes in my application.
namespace App\Database;
use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
class DatabaseManager extends BaseDatabaseManager {
public static bool $isDebugEnabled = false;
// ...
// Overwrites some methods of the base class which prints additional diagnostic information
// if `self::$isDebugEnabled === true`.
// ...
}
and
namespace App\Database;
use Illuminate\Database\Connection as BaseConnection;
class Connection extends BaseConnection {
public static bool $isDebugEnabled = false;
/**
* Get a new query builder instance.
*
* This returns our own extended query builder with instrumentation.
*
* #return Query\Builder
*/
public function query(): Query\Builder {
return new Query\Builder(
$this, $this->getQueryGrammar(), $this->getPostProcessor()
);
}
// ...
// Overwrites some methods of the base class which prints additional diagnostic information
// if `self::$isDebugEnabled === true`.
// ...
}
and
namespace App\Database\Query;
use Illuminate\Database\Query\Builder as BaseBuilder;
class Builder extends BaseBuilder {
public static bool $isDebugEnabled = false;
// ...
// Overwrites some methods of the base class which prints additional diagnostic information
// if `self::$isDebugEnabled === true`.
// ...
}
How do I register my custom DatabaseManager and Connection with the Laravel Service Container instead of the original parent classes? The original classes are registered in \Illuminate\Foundation\Application::registerCoreContainerAliases() which is called by the constructor of the Application object, i.e. these classes are registered at a very early stage.
I thought that I could "re-register" my classes in \App\Providers\AppServiceProvider::register like this
namespace App\Providers;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register() {
// Overwrite core container services with our own classes
// for debugging purposes
// See \Illuminate\Foundation\Application::registerCoreContainerAliases()
$this->app->alias('db', \App\Database\DatabaseManager::class);
$this->app->alias('db.connection', \App\Database\Connection::class);
}
}
but it did not work. The original classes are still being used. I used app->alias, because this is what registerCoreContainerAliases originally uses, too. But I also tried ->bind with no success.
After some debugging I found out that ->alias and ->bind do not overwrite a previously registered binding, but simply add a new binding to the end of the list. Hence, the previously registered services by the Laravel core application are still preferential.
What is the correct solution?
Related
I was looking around the Laravel framework and some of their products and I noticed that Cashier is using the Casheir class with static methods compared to Socialite, which is used as a facade.
What are the benefits/downsides of building it one or the other way, or is there none at all?
I would like to build something myself, but I don't want to start building it as a class with static methods if building it as a facade is a better solution.
When you may need multiple implementations, a single interface can be defined through facade to simplify the code
Building it as a class with static methods:
When you have multiple classes you have to do something like this:
CashierOne::method, CashierTwo::method ....
Used as a facade:
According to what you bind to the container to switch the implementation
You only need to call through an interface:
// Define a Cashier Facade
class Cashier extends Facade
{
/**
* Get the registered name of the component.
*
* #return string
*/
protected static function getFacadeAccessor()
{
return 'cashier';
}
}
// In CashServiceProvider
$this->app->singleton('cashier', function ($app) {
return new CashierManager ($app);
});
// In CashierManager
public function gateway($name = null)
{
// get cashier implementation by name
}
public function __call($method, $parameters)
{
return $this->gateway()->$method(...$parameters);
}
// In Controller
Cashier::method
In addition, the facade is easier to test, check:
https://laravel.com/docs/5.8/facades#how-facades-work
While testing:
While checkout items from my website, need to mock confirmation... so we can then continue processing the order. Where the testing can be done..
How would i swap out good code for a mock? such as:
$gateway = Omnipay::create('paypal');
$response = $gateway->purchase($request['params'])->send();
if ($response->isSuccessful()) { ... etc ...
How is this possible?
While i have created tests, my knowledge in the area of mocking is basic
As far as it depends t mocking, you don't need to know exact response, you just need to know inputs and outputs data and you should replace your service (Paypal in this case) in laravel service provider. You need some steps like bellow:
First add a PaymentProvider to your laravel service provider:
class AppServiceProvider extends ServiceProvider
{
...
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->app->bind(PaymentProviderInterface::class, function ($app) {
$httpClient = $this->app()->make(Guzzle::class);
return new PaypalPackageYourAreUsing($requiredDataForYourPackage, $httpClient);
});
}
...
}
Then in your test class you should replace your provider with a mock version of that interface:
class PaypalPackageTest extends TestCase
{
/** #test */
public function it_should_call_to_paypal_endpoint()
{
$requiredData = $this->faker->url;
$httpClient = $this->createMock(Guzzle::class);
$paypalClient = $this->getMockBuilder(PaymentProviderInterface::class)
->setConstructorArgs([$requiredData, $httpClient])
->setMethod(['call'])
->getMock();
$this->instance(PaymentProviderInterface::class, $paypalClient);
$paypalClient->expects($this->once())->method('call')->with($requiredData)
->willReturn($httpClient);
$this->assertInstanceOf($httpClient, $paypalClient->pay());
}
}
This is the approach I usually take when I have to mock methods that contain calls to external libraries (such as Omnipay in your case).
Your snippet isn't very extensive, but I'll assume your class looks something like this:
class PaymentProvider
{
public function pay($request)
{
$gateway = Omnipay::create('paypal');
$response = $gateway->purchase($request['params'])->send();
if ($response->isSuccessful()) {
// do more stuff
}
}
}
What I would do is refactor the class, so that the call to the external library is inside a separate method:
class PaymentProvider
{
protected function purchaseThroughOmnipay($params)
{
$gateway = Omnipay::create('paypal');
return $gateway->purchase($params)->send();
}
public function pay($request)
{
$response = $this->purchaseThroughOmnipay($request['params']);
if ($response->isSuccessful()) {
// do more stuff
}
}
}
Then, after this refactoring, in the test class we can take advantage of the many possibilities PHPunit's getMockBuilder gives us:
<?php
use PHPUnit\Framework\TestCase;
class PaymentProviderTest extends TestCase
{
protected $paymentProvider;
protected function setUp()
{
$this->paymentProvider = $this->getMockBuilder(\PaymentProvider::class)
->setMethods(['pay'])
->getMock();
}
public function testPay()
{
// here we set up all the conditions for our test
$omnipayResponse = $this->getMockBuilder(<fully qualified name of the Omnipay response class>::class)
->getMock();
$omnipayResponse->expects($this->once())
->method('isSuccessful')
->willReturn(true);
$this->paymentProvider->expects($this->once())
->method('purchaseThroughOmnipay')
->willReturn($omnipayResponse);
$request = [
// add relevant data here
];
// call to execute the method you want to actually test
$result = $this->paymentProvider->pay($request);
// do assertions here on $result
}
}
Some explanation of what's happening:
$this->paymentProvider = $this->getMockBuilder(\PaymentProvider::class)
->setMethods(['pay'])
->getMock();
This gives us a mock instance of the Payment class, for which pay is a "real" method whose actual code is actually executed, and all other methods (in our case, purchaseThroughOmnipay is the one we care about) are stubs for which we can override the return value.
In the same way, here we are mocking the response class, so that we can then control its behavoir and influence the flow of the pay method:
$omnipayResponse = $this->getMockBuilder(<fully qualified name of the Omnipay response class>::class)
->getMock();
$omnipayResponse->expects($this->once())
->method('isSuccessful')
->willReturn(true);
The difference here is that we are not calling setMethods, which means that all the methods of this class will be stubs for which we can override the return value (which is exactly what we are doing for isSuccessful).
Of course, in case more methods of this class are called in the pay method (presumably after the if), then you will probably have to use expect more than once.
I am trying to do a hello world service provider with the new Laravel 5.4.
I have created the following service provider file:
//File: app/TestProvider/TestServiceProvider.php
namespace App\TestProvider;
use Illuminate\Support\ServiceProvider;
class TestServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
$this->app->bind('Test', function ($app) {
return new Test();
});
}
}
I have created a simple class under the same namespace:
//File: app/TestProvider/Test.php
namespace App\TestProvider;
class Test
{
/**
* Register bindings in the container.
*
* #return void
*/
public function helloWorld()
{
echo "hello world";
}
}
The problem is, this is not registering. The register method is executing as when I put a breaker before the 'bind' method, it executes:
public function register()
{
dd("BREAKER");
$this->app->bind('Test', function ($app) {
return new Test();
});
}
So this outputs "BREAKER" as expected. However if I put the breaker in the closure, nothing happens which suggests for some reason, that 'bind' method isn't being executed??
Any ideas?
EDIT:
Just some further info: I know that the Test class is registered and in the correct namespace as I can do:
dd(new Test());
in the registration method, and it outputs the resource id as expected.
Explanation
The closure provided only runs when the binding is being resolved. That's why it's a closure, it can be saved in the service container and resolved at any time while the program runs.
Solution
To see the resolved binding, create a controller and resolve the class in that controller:
// File: app/Http/Controllers/TestController.php
namespace App\Http\Controllers;
// This isn't the best way, but it works. See the best way below
class TestController extends Controller {
public function index()
{
return \App::make('Test')->helloWorld();
}
}
Of course, don't forget to register the route:
// File: routes/web.php
Route::get('/', 'TestController#index');
The binding will resolve when you hit the homepage.
However, as I said, it's not the best way, so here I prepared a better way. Change the way you register the binding:
// File: app/Providers/TestProvider.php
namespace App\TestProvider;
use Illuminate\Support\ServiceProvider;
use App\TestProvider\Test;
// Better way
class TestServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
// Note: we bind the exact complete class name!
$this->app->bind(Test::class, function ($app) {
return new Test();
});
}
}
After this change the controller so that it looks like this:
namespace App\Http\Controllers;
use App\TestProvider\Test;
class TestController extends Controller {
/**
* #var Test $test
*/
private $test;
// Let Laravel resolve the dependency on constructing the class
public function __construct(Test $test)
{
$this->test = $test;
}
public function index()
{
return $this->test->helloWorld();
}
}
You will see that the exact same thing happens, but it looks more elegant and avoids conflicts.
Details
Laravel gives only a high level overview of the service container, which doesn't help to learn how it works on the inside. The best way to see that is to go down the call stack.
When you do that, you find that Laravel registers every class in the project in the service container. That means that whether you create a service provider or not, the class will be in the container. How exactly?
When you run php artisan optimize, Laravel creates files that have array with all the classes of the project. When you run the app, after registering everything from the service providers, Laravel registers the rest of the classes from that file.
That means that in your case, if you don't specifically register the Test class, it will still be resolvable. Basically, you only need to register classes that need some specific instructions to be resolved.
So how does Laravel resolve the dependencies?
When you run \App::make(Test::class) or inject dependency via type hinting in the constructor (the "better way" from my solution), Laravel looks for that dependency among the bindings.
When it finds the dependency, it resolves either the closure associated to it or the constructor of the class directly.
When it resolves the constructor directly, it looks for type hints among the constructor parameters and recursively resolves all of them until there's nothing else to resolve.
After that it returns the resolved class.
Of course, bear in mind that for Laravel to analyze the contructor of a class, it needs to be resolved via the service container in the first place. You can't just call $test = new Test(); and expect Laravel to do all the magic :)
Conclusion
This is a rather quick overview of Laravel's service container. The best way for you to learn it is, of course, studying the sources for yourself. It's truly elegant and it uses PHP's functionality to the fullest.
I really hope this shed some light on the service container for you and can help you in the future :)
The closure passed to the bind() method is not executed until you actually attempt to resolve the alias you are binding.
So, if you dd('breaker') inside the closure, this won't actually get executed until Test is resolved (whatever your preferred resolution method is):
Service provider:
// bind the closure to the 'Test' alias
public function register()
{
$this->app->bind('Test', function ($app) {
dd("BREAKER");
return new Test();
});
}
Code that resolve Test alias:
// different ways of resolving the alias out of the container.
// any of these will execute the bound closure.
$test = resolve('Test');
$test = app('Test');
$test = app()->make('Test');
$test = \App::make('Test');
try:
$this->app->bind(Test::class, function ($app) {
return new Test();
});
I want to implement some extra features to Illuminate\Translate\Translator.
So, i create my folder in ~/vendor directory, place there My/Traslator class, that will implement Symfony\Component\Translation\TranslatorInterface. Right?
Is it OK to extend laravel translator class (a lot of functionality will be duplicated otherwise) in my package?
If it is ok - it will be necessary to tie to current laravel version to keep code stable. But what will happen in case enduser laravel version will differ from one required in my package?
What should i do then to make laravel use my translator class in application (facades,etc)?
Make a Translator class and make it extend Illuminate\Translation\Translator
<?php
namespace App\Helpers;
use Illuminate\Translation\Translator as LaravelTranslator;
class Translator extends LaravelTranslator
{
// here you can overwrite any functions you want/need
}
Create your own TranslationServiceProvider inside app/providers (just copy the laravel translation service provider and change the line where it uses Translator with your own Translator class where you have overwritten what you needed)
<?php
namespace App\Providers;
use App\Helpers\Translator; // <= Your own class
use Illuminate\Translation\FileLoader;
use Illuminate\Support\ServiceProvider;
class TranslationServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerLoader();
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];
// When registering the translator component, we'll need to set the default
// locale as well as the fallback locale. So, we'll grab the application
// configuration so we can easily get both of these values from there.
$locale = $app['config']['app.locale'];
$trans = new Translator($loader, $locale);
$trans->setFallback($app['config']['app.fallback_locale']);
return $trans;
});
}
/**
* Register the translation line loader.
*
* #return void
*/
protected function registerLoader()
{
$this->app->singleton('translation.loader', function ($app) {
return new FileLoader($app['files'], $app['path.lang']);
});
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides()
{
return ['translator', 'translation.loader'];
}
}
Comment out or delete the Laravels translator service line inside config/app.php:
//Illuminate\Translation\TranslationServiceProvider::class,
Add your own Provider in that same array
App\Providers\TranslationServiceProvider::class,
This page has more information: http://laravel.com/docs/5.0/extending#container-based-extension
So what you need to do is:
Extend the built-in class from the vendor directory
Create a new service provider that add your translation class to the service container
Replace Laravel’s translation service provider in your config/app.php file with the namespace of your translation service provider
Now when you ask for the translation service provider out of the service container—either directly (app('translator')) or with the Lang façade, it will return your translation class rather than Laravel’s.
I wanted to create a custom basepath helper to replace the original zf2 basepath view helper.
So if i call $this->basepath, it will use my custom basepath instead of the original one. I am not sure if this is can be done. I want my custom basepath extends the original basepath class too.
I have found some answers on how to create custom helpers and how to register them in module.php or module.config.php
But i can't find any similar questions on how to override the original helpers!
Factory definition of the basepath view helper is declared as a hardcoded invokable in HelperPluginManager (on line 45) however this definition also overridden in ViewHelperManagerFactory (line 80 to 93) because BasePath view helper requires the Request instance from ServiceLocator:
$plugins->setFactory('basepath', function () use ($serviceLocator) {
// ...
})
I strongly recommend extending the built-in basepath helper with a different name (MyBasePath for example) instead of trying to override the existing one. Overriding that native helper may produce some unexpected headaches later (think about 3rd party modules which uses that helper to work).
For your question; yes, it is possible.
Create the Application\View\Helper\BasePath.php helper class like below:
namespace Application\View\Helper;
use Zend\View\Helper\BasePath as BaseBasePath; // This is not a typo
/**
* Custom basepath helper
*/
class BasePath extends BaseBasePath
{
/**
* Returns site's base path, or file with base path prepended.
*/
public function __invoke($file = null)
{
var_dump('This is custom helper');
}
}
And override the factory in the onBootstrap() method of the Module.php file like below:
namespace Application;
use Zend\Mvc\MvcEvent;
use Application\View\Helper\BasePath; // Your basepath helper.
use Zend\View\HelperPluginManager;
class Module
{
/**
* On bootstrap for application module.
*
* #param MvcEvent $event
* #return void
*/
public function onBootstrap(MvcEvent $event)
{
$services = $event->getApplication()->getServiceManager();
// The magic happens here
$services->get('ViewHelperManager')->setFactory('basepath', function (HelperPluginManager $manager) {
$helper = new BasePath();
// Here you can do whatever you want with the instance before returning
return $helper;
});
}
}
Now you can try in any view like this:
echo $this->basePath('Bar');
This is not a perfect solution but it should work.