Laravel - Repository Pattern issues - php

I've got my Laravel application using the repository pattern. I also have an abstract class called EloquentRepository which contains basic methods. All of my repositories have an update() method, where I simply update a model using an ID and array:
abstract class EloquentRepository {
public function update($id, array $array) {
$this->model->whereId($id)->update($array);
}
}
Now, I also have a Server repository:
interface ServerRepository {
public function update($id, array $options);
}
class EloquentServerRepository extends EloquentRepository implements ServerRepository {
protected $model;
public function __construct(Server $model)
{
$this->model = $model;
}
}
So now, I don't have to add the update() method to my EloquentServerRepository, nor any other Repositories which need to do this (quite a few).
However, there is one repository which does have an update feature, but I'd like it to do something "custom". Lets say it's the User repository:
interface UserRepository {
public function update($id, array $options, $status);
}
class EloquentUserRepository extends EloquentRepository implements UserRepository {
protected $model;
public function __construct(User $model)
{
$this->model = $model;
}
public function update($id, array $options, $status)
{
$this->model->setStatus($status);
$this->model->whereId($id)->update($options);
}
}
So now, I have my User repository requiring a status with every update.
However, I get the error:
Declaration of EloquentUserRepository::update() should be compatible with EloquentRepository::update($id, array $array).
Why is this, surely my interface specifies what the declaration should be?

You can get passed that error by making $status optional by giving it default value, for example:
public function update($id, array $options, $status = null)
Without it being optional (with default value) you're saying this method needs to have a third parameter, which violates the contract set by ServerRepository

It's because you are extending EloquentUserRepository where you have the update method like this:
public function update($id, array $array) {
$this->model->whereId($id)->update($array);
}
In this case you are also implementing the UserRepository interface but according to the base class' update method your update method has a different signature, which is as given below:
public function update($id, array $options, $status);
So, the error is rising because you've different method signatures. While you may can make both method's signature same probably using an optional parameter like this:
// EloquentUserRepository
public function update($id, array $array, $status = null) {
$this->model->whereId($id)->update($array);
}
// interface UserRepository
interface UserRepository {
public function update($id, array $options, $status = null);
}
But I would suggest to use only one interface or abstract class and override the method in your EloquentUserRepository for the different use case. Which would look like this:
abstract class EloquentRepository {
public function update($id, array $array, $status = null) {
$this->model->whereId($id)->update($array);
}
}
// Only extend the EloquentRepository and override the update method
class EloquentUserRepository extends EloquentRepository {
protected $model;
public function __construct(User $model)
{
$this->model = $model;
}
// re-declare the method to override
public function update($id, array $options, $status = null)
{
$this->model->setStatus($status);
$this->model->whereId($id)->update($options);
}
}
Or change EloquentRepository a little, for example:
abstract class EloquentRepository {
public function update($id, array $array, $status = null) {
if(!is_null($status)) {
$this->model->setStatus($status);
}
$this->model->whereId($id)->update($array);
}
}

Related

Proper way to inject a service on a controller

I am implementing the Repository Pattern (service) in a Laravel application and I have some doubts about the usage of interfaces with these services.
I have created an interface called CRUD (code bellow) to serve as a way to always keep the same names for the services that are going to implement CRUD methods.
<?php
namespace App\Interfaces;
interface CRUD
{
public function create(array $data);
public function update(int $id, array $data);
public function delete(string $ids);
};
Bellow there's an example of how I call my service and the service itself, and that's where my doubts are. Usually I'll see people witing an interface for each service and demanding the controller to have injected an objet of that type. Because of that, people will have to bind a specific type (interface) to the controller. It seems redundant and thus I simply passed the service I need.
Now, is this ok or I should pass the CRUD interface to the controller in this case? Or should I even create another interface specifically for each service?
<?php
namespace App\Http\Controllers\Cms;
use App\Http\Controllers\Controller;
use App\Http\Requests\GroupRequest;
use App\Models\Group;
use App\Services\GroupsService;
use Illuminate\Http\Request;
class GroupsController extends Controller
{
private $service;
public function __construct(GroupsService $service)
{
$this->service = $service;
}
public function store(GroupRequest $request)
{
$result = $this->service->create($request->all());
return redirect()->back()->with('response', $result);
}
public function update(GroupRequest $request, $id)
{
$result = $this->service->update($id, $request->all());
return redirect()->back()->with('response', $result);
}
public function destroy($groups_id)
{
$result = $this->service->delete($groups_id);
return redirect()->back()->with('response', $result);
}
}
<?php
namespace App\Services;
use App\Models\Group;
use App\Interfaces\CRUD;
use Exception;
class GroupsService implements CRUD
{
public function listAll()
{
return Group::all();
}
public function create(array $data)
{
$modules_id = array_pop($data);
$group = Group::create($data);
$group->modules()->attach($modules_id);
return cms_response(trans('cms.groups.success_create'));
}
public function update(int $id, array $data)
{
try {
$modules_ids = $data['modules'];
unset($data['modules']);
$group = $this->__findOrFail($id);
$group->update($data);
$group->modules()->sync($modules_ids);
return cms_response(trans('cms.groups.success_update'));
} catch (\Throwable $th) {
return cms_response($th->getMessage(), false, 400);
}
}
public function delete(string $ids)
{
Group::whereIn('id', json_decode($ids))->delete();
return cms_response(trans('cms.groups.success_delete'));
}
private function __findOrFail(int $id)
{
$group = Group::find($id);
if ($group instanceof Group) {
return $group;
}
throw new Exception(trans('cms.groups.error_not_found'));
}
}
If you want to use Repository Design Patteren You have to create seprate Interface for each service accroing to SOLID Principle. You have to create custom service provider and register your interface and service class and then inject interface in construtor of controller.
You can also follow below article.
https://itnext.io/repository-design-pattern-done-right-in-laravel-d177b5fa75d4
I did something with repo pattern in laravel 8 you might be interested:
thats how i did it:
first of all, you need to implement a provider
in this file i created the binding:
App\ProvidersRepositoryServiceProvider.php
use App\Interfaces\EventStreamRepositoryInterface;
use App\Repositories\EventStreamRepository;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(EventStreamRepositoryInterface::class, EventStreamRepository::class);
}
}
then in file:
app\Interfaces\EventStreamRepositoryInterface.php
interface EventStreamRepositoryInterface {
public function index();
public function create( Request $request );
public function delete($id);
}
in file:
App\Repositories\EventStreamRepository.php
class EventStreamRepository implements EventStreamRepositoryInterface{
public function index()
{
return EventStream::with(['sessions'])
->where([ ["status", "=", 1] ] )
->orderBy('created_at', 'DESC')
->get();
}
public function create(Request $request)
{
request()->validate([
"data1" => "required",
"data2" => "required"
]);
$EventStream = EventStream::create([
'data1' => request("data1"),
'data2' => request('data2')
]);
return $EventStream->id;
}
public function delete($id)
{
return EventStream::where('id', $id)->delete();
}
}
in file:
App\Http\Controllers\EventStreamController.php
use App\Interfaces\EventStreamRepositoryInterface;
class EventStreamController extends Controller{
private EventStreamRepositoryInterface $eventStreamRepository;
public function __construct(EventStreamRepositoryInterface $eventStreamRepository)
{
$this->eventStreamRepository = $eventStreamRepository;
}
public function index():JsonResponse
{
$this->eventStreamRepository->index();
}
public function store(Request $request ):JsonResponse
{
$this->eventStreamRepository->create($request);
}
public function destroy($id):JsonResponse
{
$this->eventStreamRepository->delete($id);
}
}//class
note: i think i removed all unnecessary -validations- and -returns- in controller for better reading.
Hope it helps!!

Laravel route resolve custom data type

I have the following routes in routes/api.php:
Route::get('items/{item}', function(Guid $item) {...});
Route::get('users/{user}', function(Guid $user) {...});
Since Guid is a custom type, how can I resolve that via dependency injection? As shown, the route parameter {item} differs from the callback parameter type-hint:Guid so it can not be automatically resolved.
That's what I've tried in app/Providers/AppServiceProvider.php:
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->app->bind(Guid::class, function(Application $app, array $params) {
return Guid::fromString($params[0]);
});
}
}
I'd expect $params to be something like this: [ 'item' => 'guid' ] -- but it is: [].
You can make use of explicit binding Laravel Routing:
in RouteServiceProvider::boot():
public function boot()
{
Route::model('item', Guid $item);
Route::model('user', Guid $user);
}
If Guid is not a model use a Closure to map onto the string:
Route::bind('user', function ($value) {
return Guid::fromString($value);
});
UPDATED
And I found another way, much better - implement UrlRoutable contract Lavaravel API:
<?php
namespace App\Models;
use Illuminate\Contracts\Routing\UrlRoutable;
class Guid implements UrlRoutable
{
private string $guid;
public function setGuid(string $guid)
{
$this->guid = $guid;
return $this;
}
public function getGuid(): string
{
return $this->guid;
}
public static function fromString(string $guid): self
{
//you cannot set props from constructor in this case
//because binder make new object of this class
//or you can resolve constructor depts with "give" construction in ServiceProvider
return (new self)->setGuid($guid);
}
public function getRouteKey()
{
return $this->guid;
}
public function getRouteKeyName()
{
return 'guid';
}
public function resolveRouteBinding($value, $field = null)
{
//for using another "fields" check documentation
//and maybe another resolving logic
return self::fromString($value);
}
public function resolveChildRouteBinding($childType, $value, $field)
{
//or maybe you have relations
return null;
}
}
And, with this, you can use routes like you want as Guid now implements UrlRoutable and can turn {item} (or whatever) URL-path sub-string markers into Guids per dependency injection (by the type-hint as you asked for it):
Route::get('items/{item}', function(Guid $item) {
return $item->getGuid();
});
BTW: NEVER EVER use closures in routes as you cannot cache closure routes - and routes are good to be optimized, and caching helps with that in Laravel routing.
simple helper to utilize route binding callback.
if (!function_exists('resolve_bind')) {
function resolve_bind(string $key, mixed $value) {
return call_user_func(Route::getBindingCallback($key), $value);
}
}
usage
resolve_bind('key', 'value');

Laravel 7: Service provider bind a class with a constructor parameter model

I've created a Service Provider with a class that has a model passed into the constructor.
The model needs to be a specific record based off the $id taking from the URL eg /path/{$id}
How can I use the requested model in the Service Provider?
An option is to pass the model into the execute method but for now I'll need to pass it into the construct.
MyController
class MyController {
public function show(MyClass $myClass, $id)
{
$model = MyModel::find($id);
return $myClass->execute();
}
}
MyClass
class MyClass
{
$private $myModel;
public function __construct(MyModel $myModel)
{
$this->myModel = $myModel;
}
public function execute()
{
//do something fun with $this->myModel
return $theFunStuff;
}
}
MyServiceProvider
public function register()
{
$this->app->singleton(MyClass::class, function ($app) {
return new MyClass(/* How can I use $myModel? */);
});
}
I don't see any value / reason to use a singleton here.
The service provider registers the singleton before your route is resolved, so there is no way to pass the $model from the controller into the register method. I would remove the service provider and do the following:
From the docs:
If some of your class' dependencies are not resolvable via the
container, you may inject them by passing them as an associative array
into the makeWith method:
$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);
So in your case something like this:
public function show($id)
{
return app()->makeWith(MyClass::class, ['myModel' => MyModel::find($id)])->execute();
}
Or shorter with the help of route model binding:
public function show(MyModel $myModel)
{
return app()->makeWith(MyClass::class, compact('myModel'))->execute();
}
Note that the argument names passed to makeWith have to match the parameter names in the class constructor.

Laravel ::__construct() must implement interface

I'm using Laravel 5.4 And I'm trying to inject a $order class into a trait that's going to implemented by a model. Like this:
class Forum extends Model
{
use Orderable;
The constructor of the trait looks like this:
trait Orderable
{
public $orderInfo;
public function __construct(OrderInterface $orderInfo)
{
$this->orderInfo = $orderInfo;
}
My service provider looks like this:
public function register()
{
$this->app->bind(OrderInterface::class, function () {
return new Order(new OrderRepository());
});
$this->app->bind(OrderRepositoryInterface::class, function () {
return new OrderRepository();
});
}
The constructor of my Order class looks like this:
public function __construct(OrderRepositoryInterface $orderInfo)
{
$this->orderInfo = $orderInfo;
}
But I receive the error:
Type error: Argument 1 passed to App\Forum::__construct() must implement interface Project\name\OrderInterface, array given, called in /home/vagrant/Code/Package/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php on line 268
The OrderRepository class is implementing the OrderRepositoryInterface. The Order class is implementing the OrderInterface interface.
App\Forum is the model that uses the Orderable trait.
What could I be doing wrong here?
You are extending Model. This class already has a __construct that you need to use. This __construct expects array $attributes = [] as the first argument.
So in your __construct you also need to have this as the first argument and pass this to the parent class:
public function __construct(array $attributes = [], OrderRepositoryInterface $orderInfo)
{
$this->orderInfo = $orderInfo;
parent::__construct($attributes);
}
However you can work around using __construct in laravel using boot.
For example in a Model:
class Forum extends Model
{
protected static function boot()
{
parent::boot();
// Do something
}
}
Or in a Trait:
trait Orderable
{
public static function bootOrderableTrait()
{
static::created(function($item){
// Do something
});
}
}
In PHP it's not possible to have multiple constructors. If you will look to Model:
public function __construct(array $attributes = [])
it expect array. That's why I assume that somewhere in Model array passed to constructor instead of 'OrderInterface'.

Call createForm() and generateUrl() from service in Symfony2

I would like have access to controller methods from my custom service. I created class MyManager and I need to call inside it createForm() and generateUrl() functions. In controller I can use: $this->createForm(...) and $this->generateUrl(...), but what with service? It is possible? I really need this methods! What arguments I should use?
If you look to those two methods in Symfony\Bundle\FrameworkBundle\Controller\Controller class, you will see services name and how to use them.
public function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
{
return $this->container->get('router')->generate($route, $parameters, $referenceType);
}
public function createForm($type, $data = null, array $options = array())
{
return $this->container->get('form.factory')->create($type, $data, $options);
}
Basically, you class need services router and form.factory for implementing functionality. I do not recommend passing controller to your class. Controllers are special classes that are used mainly by framework itself. If you plan to use your class as service, just create it.
services:
my_manager:
class: Something\MyManager
arguments: [#router, #form.factory]
Create a constructor with two arguments for services and implement required methods in your class.
class MyManager
{
private $router;
private $formFactory;
public function __construct($router, $formFactory)
{
$this->router = $router;
$this->formFactory = $formFactory;
}
// example method - same as in controller
public function createForm($type, $data = null, array $options = array())
{
return $this->formFactory->create($type, $data, $options);
}
// the rest of you class ...
}
assuming you are injecting the service into your controller , you can pass the controller object to your service function
example
class myService
{
public function doSomthing($controller,$otherArgs)
{
$controller->generateForm();
}
}
class Mycontroller extends Controller
{
public function indexAction()
{
$this->get("my-service")->doSomthing($this,"hello");
}
}

Categories