Contextual binding of interface via config in laravel 5 - php

Lets assume I have an interface like so:
interface RepositoryInterface{
public function getById($id);
}
This interface gets implemented by X amount of classes.
As an example:
class SqliteRepository implements RepositoryInterface{
public function getById($id)
{
return $id;
}
}
I also have a config file in the config folder(do note, this is not the database.php file, it's whole different file):
'default' => 'sqlite',
'connections' => [
'sqlite' => [
'database' => env('DB_DATABASE', storage_path('database.sqlite')),
],
'some_other_db' => [
'database' => env('DB_DATABASE', storage_path('some_other_db')),
],
],
The connections itself can be anything. A database, an API, heck even a csv file.
The main idea behind this is that I can switch in between storage mediums simply by changing the config. Don't ask me why I'm not using the default laravel database file, it's a long story.
The problem:
I want to be able to inject different implementations of the RepositoryInterface into controllers based on that config file, something along the lines of this:
if(Config::get('default') == 'sqlite')
{
// return new SqliteRepository
}
Obviously the way to go here would be Service Providers. However I'm not exactly sure how to approach this issue.
I mean I could do something along the lines of this:
class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
if(Config::get('storage') == 'sqlite')
{
$this->app->singleton(SqliteRepository::class, function ($app) {
return new SqliteRepository(config('SqliteRepository'));
});
}
}
}
But it feels a little wrong, not to mention that it gives me zero error control. I don't want to be throwing errors in the ServiceProvider. I need some sort of contextual binding or something along those lines. I have read the the documentation regarding contextual binding but it's no exactly what I'm looking for, as it refers rather to concrete implementations of classes based on what controller uses them.
I was thinking more of an abstract factory type of deal, but, again, I'm not sure how to fit into laravel's way of doing things.
Any pointing in the right direction is appreciated.

interface RepositoryInterface{
public function getById();
}
...
...
class SqliteRepository implements RepositoryInterface{
public function getById()
{
return 1;
}
}
...
...
class CsvRepository implements RepositoryInterface{
public function getById()
{
return 2;
}
}
...
...
class MonkeyPooRepository implements RepositoryInterface{
public function getById()
{
return 3;
}
}
...
...
use RepositoryInterface;
class Controller {
public function __construct( RepositoryInterface $repo ) {
$this->repo = $repo;
}
public function index()
{
dd($this->repo->getById());
}
}
on your app provider;
public function register()
{
$this->app->bind( RepositoryInterface::class, env('REPOSITORY', 'MonkeyPooRepository' ) );
}
index method would return (int)3

Related

Laravel 7 - How do I rebind repository on Validator::extend() for tests?

I have set up a custom validation rule like this:
// Custom created RepositoryProvider.php also registered in app.php 'providers' array.
public $bindings = [
UserRepository::class => EloquentUserRepository::class,
];
public function boot(UserRepository $userRepository)
{
Validator::extend('user_email_unique', function($attribute, $value, $parameters) use($userRepository) {
return !$userRepository->findByEmailAddress($value)->exists();
});
}
// my test
class SignUpUserActionTest extends TestCase
{
public function setUp() : void
{
parent::setUp();
$this->app->bind(UserRepository::class, function() {
return new UserRepositoryMock();
});
}
}
In my test I rebind the UserRepository to a mock. It works fine in for the fetching of data, but it maintains the original binding for the validation extension and does not rebind the repository used. They therefore use two different implementations when unit tests are run.
How can I extend the validator so that the automatic resolution is rebound on tests?
Thanks.
Since you do you logic in your RepositoryProvider, this will be instantiated in your parent::setUp() call and thereby fetch your UserRepository before the binding has been mocked. Move the binding before this call and i will assume you will get a different result.
public function setUp() : void
{
$this->app->bind(UserRepository::class, function() {
return new UserRepositoryMock();
});
parent::setUp();
}
EDIT
Based on your comment, the cause was right the solution was not. Resolve UserRepository in the closure, will most likely make it resolve at a time where your binding is set.
Validator::extend('user_email_unique', function($attribute, $value, $parameters) {
$userRepository = resolve(UserRepository::class);
return !$userRepository->findByEmailAddress($value)->exists();
});

In Laravel, how to give another implementation to the service container when testing?

I'm creating a Laravel controller where a Random string generator interface gets injected to one of the methods. Then in AppServiceProvider I'm registering an implementation. This works fine.
The controller uses the random string as input to save data to the database. Since it's random, I can't test it (using MakesHttpRequests) like so:
$this->post('/api/v1/do_things', ['email' => $this->email])
->seeInDatabase('things', ['email' => $this->email, 'random' => 'abc123']);
because I don't know what 'abc123' will be when using the actual random generator. So I created another implementation of the Random interface that always returns 'abc123' so I could assert against that.
Question is: how do I bind to this fake generator at testing time? I tried to do
$this->app->bind('Random', 'TestableRandom');
right before the test, but it still uses the actual generator that I register in AppServiceProvider. Any ideas? Am I on the wrong track completely regarding how to test such a thing?
Thanks!
You have a couple options:
Use a conditional to bind the implementation:
class AppServiceProvider extends ServiceProvider {
public function register() {
if($this->app->runningUnitTests()) {
$this->app->bind('Random', 'TestableRandom');
} else {
$this->app->bind('Random', 'RealRandom');
}
}
}
Second option is to use a mock in your tests
public function test_my_controller () {
// Create a mock of the Random Interface
$mock = Mockery::mock(RandomInterface::class);
// Set our expectation for the methods that should be called
// and what is supposed to be returned
$mock->shouldReceive('someMethodName')->once()->andReturn('SomeNonRandomString');
// Tell laravel to use our mock when someone tries to resolve
// an instance of our interface
$this->app->instance(RandomInterface::class, $mock);
$this->post('/api/v1/do_things', ['email' => $this->email])
->seeInDatabase('things', [
'email' => $this->email,
'random' => 'SomeNonRandomString',
]);
}
If you decide to go with the mock route. Be sure to checkout the mockery documentation:
http://docs.mockery.io/en/latest/reference/expectations.html
From laracasts
class ApiClientTest extends TestCase
{
use HttpMockTrait;
private $apiClient;
public function setUp()
{
parent::setUp();
$this->setUpHttpMock();
$this->app->bind(ApiConfigurationInterface::class, FakeApiConfiguration::class);
$this->apiClient = $this->app->make(ApiClient::class);
}
/** #test */
public function example()
{
dd($this->apiClient);
}
}
results
App\ApiClient^ {#355
-apiConfiguration: Tests\FakeApiConfiguration^ {#356}
}
https://laracasts.com/discuss/channels/code-review/laravel-58-interface-binding-while-running-tests?page=1&replyId=581880

Fatal error: Call to a member function schemaCollection() on a non-object in ...vendor/cakephp/cakephp/src/ORM/Table.php on line 421

Hi everyone and happy Easter !
I have this problem in migrating a legacy application from CakePHP 2.x to CakePHP 3.x
I am trying to load all the contents of a database table in the initialize function of the model (ConfigurationsTable.php - made into a Singleton) class. I also tried the same code in the constructor but still get the same error. Also tried moving it to a separate function but still no luck.
It works fine in CakePHP 2.x but I get a fatal error in CakePHP 3.
Code is as follows
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class ConfigurationsTable extends Table
{
private $_configurations;
public static function getInstance()
{
static $instance = null;
if ($instance === null) {
$instance = new static();
}
return $instance;
}
public function is_set($key)
{
return isset($this->_configurations->{$key});
}
public function fetch($key)
{
return $this->_configurations->{$key};
}
public function initialize(array $config)
{
$this->addBehavior('Timestamp');
$this->_configurations = new \stdClass();
$configs = $this->find('all');
foreach ($configs as $c) {
if (isset($c->key) && $c->key != '') {
$this->_configurations->{$c->key} = $c->value;
}
}
}
public function validationDefault(Validator $validator)
{
$validator
->notEmpty('key')
->add('key', [
'unique' => [
'rule' => 'validateUnique',
'provider' => 'table',
'message' => __('This configuration key already exists')
]
])
->notEmpty('value')
;
return $validator;
}
The line that's causing the error is: $configs = $this->find('all');
Can anyone provide me a solution for this ?
I need it for work..
Thanks a lot in advance
You cannot blindly apply 2.x concepts and expect it to work. Before you start working with code that you don't know about, take at least a look at the API docs
http://api.cakephp.org/3.0/class-Cake.ORM.Table.html#___construct
and having a look at the source itself to understand what the code actually does would be even better.
When manually instantiating table classes, you must at least provide a connection instance. However, what you are doing there doesn't make much sense, there's not really a need for a custom static instance getter, that's what TableRegistry::get() is there for.

Common logic between various Laravel controllers method

Let's say I have an URL like this:
/city/nyc (display info about new york city)
and another like this:
/city/nyc/streets (display a list of Street of nyc)
I can bind them to a method like this:
Route::get('city/{city}', 'CityController#showCity');
Route::get('city/{city}/streets', 'CityController#showCityStreet');
The problem is that I need to execute some checks on the city (for example if {city} is present in the database) on both methods.
I could create a method and call them in both like this:
class CityController {
private function cityCommonCheck($city) {
// check
}
public function showCity($city) {
$this->cityCommonCheck($city);
// other logic
}
public function showCityStreet($city) {
$this->cityCommonCheck($city);
// other logic
}
}
Is there any better way?
Even though you think differently, I believe a middleware is the best solution for this.
First, use php artisan make:middleware CityCheckMiddleware to create a class in App/Http/Middleware. Then edit the method to do what your check is supposed to do and add a constructor to inject the Router
public function __construct(\Illuminate\Http\Routing\Router $router){
$this->route = $router;
}
public function handle($request, Closure $next)
{
$city = $this->route->input('city');
// do checking
return $next($request);
}
Define a shorthand key in App/Http/Kernel.php:
protected $routeMiddleware = [
'auth' => 'App\Http\Middleware\Authenticate',
// ...
'city_checker' => 'App\Http\Middleware\CityCheckerMiddleware',
];
Then, in your controller:
public function __construct()
{
$this->middleware('city_checker', ['only' => ['showCity', 'showCityStreet']]);
}
I think best way to do this, you can move common logic into a Model.So your code would like below.
class CityController {
public function showCity($city) {
City::cityCommonCheck($city);
}
public function showCityStreet($city) {
City::cityCommonCheck($city);
}
}
model class
class City{
public static function cityCommonCheck($city) {
//put here your logic
}
}
In this way you could invoke cityCommonCheck function from any controller.

Override laravel 4's authentication methods to use custom hasing function

I have a table in my database with users. Their password are generated with my own custom hashing function.
How do i override the Authentication methods in laravel 4 to use my own hash class?
This is what I have been trying to do:
class CustomUserProvider implements Illuminate\Auth\UserProviderInterface {
public function retrieveByID($identifier)
{
return $this->createModel()->newQuery()->find($identifier);
}
public function retrieveByCredentials(array $credentials)
{
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value)
{
if ( ! str_contains($key, 'password')) $query->where($key, $value);
}
return $query->first();
}
public function validateCredentials(Illuminate\Auth\UserInterface $user, array $credentials)
{
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
}
}
class CodeIgniter extends Illuminate\Auth\Guard {
}
App::bind('Illuminate\Auth\UserProviderInterface', 'CustomUserProvider');
Auth::extend('codeigniter', function()
{
return new CodeIgniter( App::make('CustomUserProvider'), App::make('session'));
});
When I run the Auth::attempt method I get this error:
ErrorException: Warning: Illegal offset type in isset or empty in G:\Dropbox\Workspaces\www\video\vendor\laravel\framework\src\Illuminate\Foundation\Application.php line 352
This is how ended up solving the problem:
libraries\CustomHasherServiceProvider.php
use Illuminate\Support\ServiceProvider;
class CustomHasherServiceProvider extends ServiceProvider {
public function register()
{
$this->app->bind('hash', function()
{
return new CustomHasher;
});
}
}
libraries\CustomHasher.php
class CustomHasher implements Illuminate\Hashing\HasherInterface {
private $NUMBER_OF_ROUNDS = '$5$rounds=7331$';
public function make($value, array $options = array())
{
$salt = uniqid();
$hash = crypt($password, $this->NUMBER_OF_ROUNDS . $salt);
return substr($hash, 15);
}
public function check($value, $hashedValue, array $options = array())
{
return $this->NUMBER_OF_ROUNDS . $hashedValue === crypt($value, $this->NUMBER_OF_ROUNDS . $hashedValue);
}
}
And then I replaced 'Illuminate\Hashing\HashServiceProvider' with 'CustomHasherServiceProvider' in the providers array in app/config/app.php
and added "app/libraries" to autoload classmap in composer.json
#vFragosop was on the right path with extending Auth.
There are a couple of ways to skin the cat and here is how I would do that without replacing the default Hasher class:
Include in your app/routes.php or wherever:
use Illuminate\Auth\Guard;
Auth::extend("eloquent", function() {
return new Guard(
new \Illuminate\Auth\EloquentUserProvider(new CustomHasher(), "User"),
App::make('session.store')
);
});
Create and autoload a CustomHasher class (i.e., app/libraries/CustomHasher.php):
class CustomHasher extends Illuminate\Hashing\BcryptHasher {
public function make($value, array $options = array())
{
...
}
public function check($value, $hashedValue, array $options = array())
{
...
}
}
That's it.
Warning: I can't ensure this is works out of the box and there may be a few gotchas here and there. Keep in mind Laravel 4 is still on development. Wish I could provide a more precise answer, but codebase is still going through many changes and not everything is properly documented. Anyway, you are looking for something like this:
// on config/auth.php
'driver' => 'custom'
// on start/global.php
Auth::extend('custom', function() {
// CustomUserProvider is your custom driver and should
// implement Illuminate\Auth\UserProviderInterface;
return new Guard(new CustomUserProvider, App::make('session'));
});
If this doesn't give you enough information to start, you should be able to figure it out by taking a look at those classes below:
EloquentUserProvider and DatabaseUserProvider
These classes are the currently supported authentication drivers. They should guide you on how to create your CustomUserProvider (or any name you like really).
Manager
This is the base class for anything that accepts custom drivers (including the AuthManager). It provides the methods for registering them like you do in Laravel 3.
This was the top result on Google, but these answers are insufficient for anyone on Laravel 5. Even the documentation doesn't suffice.
I've successfully replaced the Hasher for only the UserProvider. The rest of my application continues to use the very nice BcryptHasher, while user authentication uses a custom hasher. To do this, I had to study these answers, the documentation, and Laravel's source code itself. Here's what I found. Hopefully I can save someone else's full head of hair. Feel free to crosspost this to a question about Laravel 5.
First, create your custom hasher, if you haven't already. Place it wherever you'd like.
class MyCustomHasher implements Hasher {
public function make($value, array $options = []) {
return md5( $value ); // PLEASE DON'T USE MD5!
}
public function check($value, $hashedValue, array $options = []) {
if (strlen($hashedValue) === 0) {
return false;
}
return $hashedValue === $this->make($value);
}
public function needsRehash($hashedValue, array $options = []) {
return false;
}
}
Edit any registered ServiceProvider as follows...
class AppServiceProvider extends ServiceProvider {
public function boot() {
Auth::provider('eloquentCustom', function ($app, $config) {
return new EloquentUserProvider(new MyCustomHasher(), $config['model']);
});
}
}
You can replace 'eloquentCustom' with whatever you'd prefer.
Finally, edit your config/auth.php to use your custom provider. Here are the relevant parts...
return [
// ...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// ...
],
// ...
'providers' => [
'users' => [
'driver' => 'eloquentCustom', // <--- This is the only change
'model' => App\User::class,
],
// ...
],
// ...
];
Here's a little explanation, because I can't believe how obscure this was.
As you may expect, authentication is configured with config/auth.php. There are two key parts: Guards and Providers. I haven't yet bothered to learn exactly what guards do, but they seem to enforce authentication requirements. Providers are responsible for providing the necessary information to the guards. Therefore, a Guard requires a Provider. You can see that, in the default configuration, guards.web.provider is mapped to providers.users.
Laravel provides two implementations of UserProvider by default: EloquentUserProvider and DatabaseUserProvider. These correspond to the two possible values for providers.users.driver: eloquent and database, respectively. Normally, the eloquent option is chosen. EloquentUserProvider needs a Hasher, so Laravel gives it whatever the standard implementation is (ie. BcryptHasher). We override this behavior by creating our own "driver" for instantiating the Provider.
Auth is our friendly neighborhood facade. It is backed by the AuthManager. The often suggested Auth::extend() method expects a Guard (contrary to what the documentation might suggest). We have no need to mess with the Guard. Instead, we can use Auth::provider() which basically does the same thing as extend(), except it expects a Provider. So we provide a function to create our own instance of a EloquentUserProvider, giving it our custom Hasher (eg. MyCustomHasher). We also include a driver "name" that can be used in the config file.
Now back to the config file. That driver name that we just created is now a valid value for providers.users.driver. Set it there and you're good to go!
I hope this all makes sense and is useful for someone!

Categories