How can I create a controller constructor in Laravel that takes in two concrete implementations of the same interface? - laravel

Background
Note: this is using Laravel 5.3, Please don't judge.
We are trying to use dependency injection with our laravel controllers and push as much business logic into repos that are injected to controllers upon the controller instantiation.
We already have this functioning example:
class AcmeController extends Controller
{
protected $repository;
public function __construct(AcmeInterface $repository)
{
$this->repository = $repository;
}
}
inside app/Providers/RepositoryServiceProvider.php we do the binding:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
$this->app->bind(\App\Repositories\Contracts\AcmeInterface::class, \App\Repositories\OpCity\AcmeRepo::class);
}
}
and then the AcmeRepo naturally implements the AcmeInterface:
class AcmeRepo implements AcmeInterface
Question
right now we have a case where some of the data of the same model is persisted in a memory type storage (redis) and the rest is persisted in relational db storage (psql). We would like to have two separate repos where each repo is specific to its storage type, ie RedisAcmeRepo and SqlAcmeRepo
How is it possible to do this in the AcmeController constructor?
public function __construct(AcmeInterface $sqlRepo, AcmeInterface $redisRepo)
{
$this->sqlRepo = $sqlRepo;
$this->redisRepo = $redisRepo;
}

For example you may do this:
$this->app->bind(AcmeController::class, function ($app) {
return new AcmeController($app->make(sqlRepo::class), $app->make(redisRepo::class));
});
Or this:
$this->app->when(AcmeController::class)
->needs('$sqlRepo')
->give($app->make(sqlRepo::class));
$this->app->when(AcmeController::class)
->needs('$redisRepo')
->give($app->make(redisRepo::class));

based on the answers above I came up with this solution, that kind of uses the composite pattern as well (I changed the name of the repos from Acme to ShopperLogs):
<?php
interface ShopperLogInterface
{
public function getLogs($from, $to, $shopper);
}
class ShopperLogsController extends Controller
{
/**
* service
*
* #var \App\Repositories\Contracts\ShopperLogInterface
* #access protected
*/
protected $manager;
public function __construct(ShopperLogInterface $manager)
{
$this->manager = $manager;
}
}
class ShopperLogManager implements ShopperLogInterface
{
protected $sqlRepo;
protected $redisRepo;
public function __construct(ShopperLogInterface $sqlRepo, ShopperLogInterface $redisRepo)
{
$this->sqlRepo = $sqlRepo;
$this->redisRepo = $redisRepo;
}
public function getLogs($from, $to, $shopper)
{
$todayRange = //get the today part of from -- to
/**
* array of ShopperLogs
*/
$todaysLogs;
if ($todayRange) {
$this->redisRepo->getLogs($todayRange->start, $todayRange->finish, $shopper);
}
$legacyRange = //get the part of from -- to that excludes today's range
/**
* array of ShopperLogs
*/
$legacyLogs;
if ($legacyLogs) {
$this->sqlRepo->getLogs($todayRange->start, $todayRange->finish, $shopper);
}
return merge($todayRange, $legacyRange);
}
}
class ShopperLogsSqlRepo implements ShopperLogInterface
{
/**
* #var /Illuminate\Database\Eloquent\Model/ShopperLogs
*/
protected $model;
/**
* #param /Illuminate\Database\Eloquent\Model/ShopperLogs $model
*/
public function __construct(ShopperLogs $model)
{
$this->model = $model;
}
public function getLogs($from, $to, $shopper)
{
$this->model->whereLogs //do eloquent sql stuff here
}
}
class ShopperLogsRedisRepo implements ShopperLogInterface
{
/**
* #var \Redis\Model\Class
*/
protected $model;
/**
* #param \Redis\Model\Class $model
*/
public function __construct(ShopperLogs $model)
{
$this->model = $model;
}
public function getLogs($from, $to, $shopper)
{
$this->model->whereLogs //do redis stuff
}
}
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
$this->app->bind(\App\Repositories\Contracts\ShopperLogInterface::class, \App\Managers\ShopperLogManager::class);
$this->app->bind(ShopperLogsController::class, function ($app) {
return new ShopperLogsController($app->make(ShopperLogManager::class));
});
$this->app->bind(\App\Repositories\Contracts\ShopperLogInterface::class, function() {
return new \App\Managers\ShopperLogManager(new \App\Repositories\ShopperLogsSqlRepo(new \App\ShopperLog), new \App\Repositories\ShopperLogsRedisRepo(new \App\ShopperLog));
});
}
}

Related

Laravel: Stop using UUID as foreign key

Laravel is trying to use uuid field as foreign key. And I want to use foreign key with the field id. Is there any option there?
Using this trait on Model. And then it is trying to use the uuid as foreign key. But still I want to use id as foreign key.
<?php
namespace App\Library;
use Ramsey\Uuid\Uuid;
trait UsesUuid
{
/**
* #return string
*/
public function getKeyName()
{
return 'uuid';
}
/**
* #return string
*/
public function getKeyType()
{
return 'string';
}
/**
* #return false
*/
public function getIncrementing()
{
return false;
}
/**
* #param $query
* #param $uuid
* #return mixed
*/
public function scopeUuid($query, $uuid)
{
return $query->where($this->getUuidName(), $uuid);
}
/**
* #return string
*/
public function getUuidName()
{
return property_exists($this, 'uuidName') ? $this->uuidName : 'uuid';
}
/**
* #return string
*/
public function getRouteKeyName()
{
return property_exists($this, 'uuidName') ? $this->uuidName : 'uuid';
}
/**
*
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->{$model->getUuidName()} = Uuid::uuid4()->toString();
});
}
}
There is nothing special about this trait. You can make your own trait with id instead of uuid and everything will work fine.
The issue came from methods getIncrementing() and getKeyName(). Laravel calls getKeyName() in amount of built-in functions to interacts with relationships, also other actions like delete(), route bindings, etc.
You should allow any models which uses this trait to custom the Primary key (PK), so that uuid is only common column at all.
Your trait definition is force PK column as uuid.
Below is my recommended for the trait
<?php
namespace App\Library;
use Ramsey\Uuid\Uuid;
trait UsesUuid
{
/* Override this method to set `uuid` as PK */
public function isUuidAsPrimaryKey()
{
return false;
}
/**
* #return string
*/
public function getKeyName()
{
return $this->isUuidAsPrimaryKey() ? $this->getUuidName() : $this->primaryKey;
}
/**
* #return string
*/
public function getKeyType()
{
return $this->isUuidAsPrimaryKey() ? 'string' : $this->keyType;
}
/**
* #return false
*/
public function getIncrementing()
{
return !$this->isUuidAsPrimaryKey();
}
/**
* #param $query
* #param $uuid
* #return mixed
*/
public function scopeUuid($query, $uuid)
{
return $query->where($this->getUuidName(), $uuid);
}
/**
* #return string
*/
public function getUuidName()
{
return property_exists($this, 'uuidName') ? $this->uuidName : 'uuid';
}
/**
* #return string
*/
public function getRouteKeyName()
{
return property_exists($this, 'uuidName') ? $this->uuidName : 'uuid';
}
/**
*
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->{$model->getUuidName()} = Uuid::uuid4()->toString();
});
}
}
If a model need to have uuid as PK, for example Book model
class Book extends Model
{
use UsesUuid;
public function isUuidAsPrimaryKey()
{
return true;
}
}
Please recheck the method isUuidAsPrimaryKey. If it may not be overridden (due to conflict), then use a property instead.

Can not get Request data in custom service?

I have custom service:
<?php
namespace App\Library\Services;
use Illuminate\Http\Request;
class RegisterCustomerService
{
private $request;
public function constructor(Request $request)
{
$this->request = $request;
}
public function register($role)
{
dd($this->request);
}
}
Why I can not get dd($this->request); when I do POST request:
$customer = $registerCustomerService->register('customer');
My service provider is:
class RegisterCustomerServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* #return void
*/
public function register(Request $request)
{
$this->app->bind('App\Library\Services\RegisterCustomerService', function ($app) {
return new RegisterCustomerService($request);
});
}
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
//
}
}
You don't need to bind the instance in the container because Laravel can automatically resolve the namespace and the class dependencies if you resolve an object through the container.
You can the remove the binding from the service provider and use:
$customer = app('App\\Library\\Services\\RegisterCustomerService')->register('customer');
In this way the container will resolve the Register customer service and will create that with all the needed dependencies (the request object in your example).

Model event is not triggered on laravel

I have a class that inherits a base class and uses a trait ... I will put the code below ..
The base class is using basically to do a validation before the rescue, using for this the saving event in the boot.
The trait is to tell the class to use uuid in the id attribute .. this trait uses the creating event of the boot.
In the class itself, the boot saving event is used to check if an active record exists.
In this code the trait creating event is not being triggered ... I can not do a save because uuid is not generated ... if I take the boot method in the final class the creating event is executed ...
something I'm not seeing ... does anybody have any idea what may be happening?
MAIN CLASS
class AcademicYear extends BaseModel
{
use UseUuid;
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
static::saving(function($model)
{
if($model->attributes['disable'] == false){
$model->searchActiveRecord();
}
});
}
public function searchActiveRecord(){
if ($this::where('disable', false)->count() >= 1){
throw new \App\Exceptions\OperationNotAllowed('operation not allowed', 'there is an active record', '422');
}
return true;
}
}
BASE MODEL
class BaseModel extends Model
{
/**
* If the model will be validated in saving
*
* #var bool
*/
protected static $validate = true;
/**
* Rules that will be used to validate the model
*
* #var array
*/
protected $validationRules = [];
/**
* Create a new base model instance.
*
* #param array $attributes
* #return void
*/
public function __construct($attributes = [])
{
parent::__construct($attributes);
}
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
static::saving(function($model)
{
if ($model::$validate) {
$model->validate();
}
});
}
/**
* Execute validation of model attributes.
*
* #return void
*/
public function validate()
{
$validator = Validator::make($this->attributesToArray(), $this->validationRules);
if($validator->fails()) {
throw new \App\Exceptions\OperationNotAllowed('validation failed', $validator->messages(), '422');
}
return true;
}
}
TRAIT
trait UseUuid
{
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model)
{
$model->incrementing = false;
$model->keyType = 'string';
$model->{$model->getKeyName()} = Str::uuid()->toString();
});
static::retrieved(function ($model)
{
$model->incrementing = false;
});
}
}
Your model's boot method is conflicting with the trait's boot method, because they have the same name.
From the PHP.net manual on Traits:
An inherited member from a base class is overridden by a member inserted by a Trait. The precedence order is that members from the current class override Trait methods, which in turn override inherited methods.
Current class: AcademicYear
Trait: UseUuid
Inherited class: BaseModel
If you want to use a boot method on an individual model, you'll have to alias the trait's method to something different:
class AcademicYear extends BaseModel
{
use UseUuid {
boot as uuidBoot;
}
// ...
protected static function boot()
{
static::uuidBoot();
// Your model-specific boot code here.
}
}
Be careful with where you place parent::boot(). If you call parent::boot() in both your trait and your model, BaseModel::boot() will be called more than once.

Can an Eloquent model has multiple Observer?

Hi I want write a trait to add an observer to model but I thought write boot method is not the right way and finnaly i find that i can boot trait like boot[TraitName] but i wonder if i add an observer with code like this:
trait CreateObserver
{
public static function bootCreateObserver()
{
static::creating(function (Model $model) {
// ...
});
}
}
can I add another observer for my model like below or it will overriding my trait observer?
class MyModel extends Model
{
use CreateObserver;
public static function boot()
{
static::creating(function ($model) {
// ...
});
}
...
}
That's not the right way. I think this might help you:
https://laravel.com/docs/5.6/eloquent#observers
You bind observers to your models using a service boot:
<?php
namespace App\Providers;
use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
User::observe(UserObserver::class);
}
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
//
}
}
Inside the observer you can add all desired functionality:
<?php
namespace App\Observers;
use App\User;
class UserObserver
{
/**
* Listen to the User created event.
*
* #param \App\User $user
* #return void
*/
public function created(User $user)
{
//
}
/**
* Listen to the User deleting event.
*
* #param \App\User $user
* #return void
*/
public function deleting(User $user)
{
//
}
}
And to elaborate. Yes it can have multiple observers. Although I never seen a useful situation for that:
public function boot()
{
User::observe(UserObserver::class);
User::observe(AuthenticableModelsObserver::class);
}
This way both the UserObserver() and AuthenticableModelsObserver() are binded to the User() model on boot.

ReflectionException in CommanderTrait.php line 59: Class App\FollowUserCommand does not exist

I Was following Laracasts video for creating follow option but when I'm clicking on Follow button on members page it is showing the above error. This is my followcontroller
<?php
namespace App\Http\Controllers;
use App\User;
use Laracasts\Commander\CommanderTrait;
use App\FollowUserCommand;
use Sentinel;
use Illuminate\Support\Facades\Input;
use App\Http\Requests;
use App\Http\Controllers\Controller;
class FollowsController extends Controller
{
use CommanderTrait;
/**
* Follow a User
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store()
{
$input = array_add(Input::all(), 'user_id', Sentinel::getuser()->id);
$this->execute(FollowUserCommand::class, $input);
return Redirect::back();
}
/**
* Unfollow a User
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}
This is my FollowUserCommand
<?php namespace App\User;
class FollowUserCommand {
public $user_id;
public $userIdToFollow;
function __construct($user_id, $userIdToFollow)
{
$this->user_id = $user_id;
$this->userIdToFollow = $userIdToFollow;
}
}
FollowUserCommandHandler
<?php namespace App;
use Laracasts\Commander\CommandHandler;
class FollowUserCommandHandler implements CommandHandler {
protected $userRepo;
function __construct(UserRepository $userRepo)
{
$this->userRepo = $userRepo;
}
public function handle($command)
{
$user = $this->userRepo->findById($command->user_id);
$this->userRepo->follow($command->userIdToFollow, $user);
return $user;
}
}
UserRepository
class UserRepository {
public function save(User $user)
{
return $user->save();
}
public function getPaginated($howMany = 4)
{
return User::orderBy('first_name', 'asc')->paginate($howMany);
}
public function findByUsername($username)
{
return User::with(['feeds' => function($query)
{
$query->latest();
}
])->whereUsername($username)->first();
}
public function findById($id)
{
return User::findOrFail($id);
}
public function follow($userIdToFollow, User $user)
{
return $user->follows()->attach($userIdToFollow);
}
}
User.php
<?php namespace App;
use Cartalyst\Sentinel\Users\EloquentUser;
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends EloquentUser {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes to be fillable from the model.
*
* A dirty hack to allow fields to be fillable by calling empty fillable array
*
* #var array
*/
protected $fillable = [];
protected $guarded = ['id'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = ['password', 'remember_token'];
/**
* To allow soft deletes
*/
use SoftDeletes;
protected $dates = ['deleted_at'];
// This function allows us to get a list of users following us
public function follows()
{
return $this->belongsToMany(self::class, 'follows', 'follower_id', 'followed_id')->withTimestamps();
}
// Get all users we are following
public function following()
{
return $this->belongsToMany('User', 'followers', 'user_id', 'follow_id')->withTimestamps();
}
}
Can anyone tell me why it is showing error even after "use App\FollowUserCommand;" has been declared in namespace.
Your namespace when declaring the FollowUserCommand class is wrong, it should be:
<?php namespace App;
class FollowUserCommand {...
And right now you have <?php namespace App\User;.

Resources