Laravel route resolve custom data type - php

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');

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!!

How to provide Symfony routing parameter programatically?

In this Symfony route
/**
* #Route("/board/{board}/card/{card}", name="card_show", methods={"GET"}, options={})
*/
public function show(Board $board, Card $card): Response
{
$card->getLane()->getBoard(); // Board instance
// ...
}
How is it possible to add the {board} parameter programatically, since it is already available in {card}? Now, I always need to add two parameters, when generating links to show action.
After some research I've found the RoutingAutoBundle (https://symfony.com/doc/master/cmf/bundles/routing_auto/introduction.html#usage) which would provide the functions I need, but it's not available for Symfony 5 anymore.
Thanks.
Okay, after some investigation I've found this question
Which lead me to this helpful answer.
My controller action (with #Route annotation) looks like this:
/**
* #Route("/board/{board}/card/{card}", name="card_show", methods={"GET"})
*/
public function show(Card $card): Response
{
}
We just have one argument ($card) in method signature, but two arguments in route.
This is how to call the route in twig:
path("card_show", {card: card.id})
No board parameter required, thanks to a custom router.
This is how the custom router looks like:
<?php // src/Routing/CustomCardRouter.php
namespace App\Routing;
use App\Repository\CardRepository;
use Symfony\Component\Routing\RouterInterface;
class CustomCardRouter implements RouterInterface
{
private $router;
private $cardRepository;
public function __construct(RouterInterface $router, CardRepository $cardRepository)
{
$this->router = $router;
$this->cardRepository = $cardRepository;
}
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
if ($name === 'card_show') {
$card = $this->cardRepository->findOneBy(['id' => $parameters['card']]);
if ($card) {
$parameters['board'] = $card->getLane()->getBoard()->getId();
}
}
return $this->router->generate($name, $parameters, $referenceType);
}
public function setContext(\Symfony\Component\Routing\RequestContext $context)
{
$this->router->setContext($context);
}
public function getContext()
{
return $this->router->getContext();
}
public function getRouteCollection()
{
return $this->router->getRouteCollection();
}
public function match($pathinfo)
{
return $this->router->match($pathinfo);
}
}
Now, the missing parameter board is provided programatically, by injecting and using the card repository. To enable the custom router, you need to register it in your services.yaml:
App\Routing\CustomCardRouter:
decorates: 'router'
arguments: ['#App\Routing\CustomCardRouter.inner']

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.

Default value for route params in generated url

I have a site which is localized into several languages. Every public route is prefixed with the locale key (e.g. /{locale}/foo/bar), which gets caught and applied by middleware.
When generating URLs to point to other pages, I end up needing to feed the current locale into every url, like so:
Foo Bar
Otherwise the output url will contain %7Blocale%7D, which breaks it. This strikes me as needlessly verbose. Is there not a way to specify a default value for a given named parameter, such that if no value is explicitly provided for 'locale' it can be defaulted to whatever the current locale is?
I've inspected the UrlGenerator class, but I don't see anything to that effect.
The Route class has a defaults property, but that only appears to be used as part of binding the route to the current request.
Ultimately, not a huge issue, just wondering if anyone has any ideas for ways to save a bit of sanity.
You can use URL defaults as well at a middleware:
use Illuminate\Support\Facades\URL;
URL::defaults(
[
'locale' => $locale
]
);
When you define your routes, use optional variables with defaults:
Routes:
Route::get('{locale?}/foo/bar', 'Controller#fooBar');
Controller:
public function __construct()
{
$this->locale = session()->has('locale') ? session('locale') : 'en';
}
public function fooBar($locale = null)
{
$locale = $locale ?: $this->locale;
}
OR:
public function fooBar($locale = 'en')
{
$locale = $locale ?: $this->locale;
}
Wherever you call your route:
Foo Bar
You could optionally put the constructor in a BaseController class that all your other controllers extend.
There may be better ways to do this, but this would keep you from having to include the locale wherever you call a route.
There isn't any built in means of doing this, but I managed to achieve the desired result by extending the UrlGenerator
<?php
namespace App\Services;
use Illuminate\Routing\UrlGenerator as BaseGenerator;
use Illuminate\Support\Arr;
class UrlGenerator extends BaseGenerator
{
protected $default_parameters = [];
public function setDefaultParameter($key, $value){
$this->default_parameters[$key] = $value;
}
public function removeDefaultParameter($key){
unset($this->default_parameters[$key]);
}
public function getDefaultParameter($key){
return isset($this->default_parameters[$key]) ? $this->default_parameters[$key] : null;
}
protected function replaceRouteParameters($path, array &$parameters)
{
if (count($parameters) || count($this->default_parameters)) {
$path = preg_replace_sub(
'/\{.*?\}/', $parameters, $this->replaceNamedParameters($path, $parameters)
);
}
return trim(preg_replace('/\{.*?\?\}/', '', $path), '/');
}
protected function replaceNamedParameters($path, &$parameters)
{
return preg_replace_callback('/\{(.*?)\??\}/', function ($m) use (&$parameters) {
return isset($parameters[$m[1]]) ? Arr::pull($parameters, $m[1]) : ($this->getDefaultParameter($m[1]) ?: $m[0]);
}, $path);
}
}
Then rebinding our subclass into the service container
class RouteServiceProvider extends ServiceProvider
{
public function register(){
parent::register();
//bind our own UrlGenerator
$this->app['url'] = $this->app->share(function ($app) {
$routes = $app['router']->getRoutes();
$url = new UrlGenerator(
$routes, $app->rebinding(
'request', function ($app, $request) {
$app['url']->setRequest($request);
}
)
);
$url->setSessionResolver(function () {
return $this->app['session'];
});
$app->rebinding('routes', function ($app, $routes) {
$app['url']->setRoutes($routes);
});
return $url;
});
}
//...
}
Then all I needed to do was inject the default locale into the UrlGenerator from the Locale middleware
public function handle($request, Closure $next, $locale = null) {
//...
$this->app['url']->setDefaultParameter('locale', $locale);
return $next($request);
}
Now route('foo.bar') will automatically bind the current locale to the route, unless another is explicitly provided.

Laravel 5 requests: Authorizing and then parsing object to controller

I am not sure if I am using this correctly, but I am utilising the requests in Laravel 5, to check if the user is logged in and if he is the owner of an object. To do this I need to get the actual object in the request class, but then I need to get the same object in the controller?
So instead of fetching it twice, I thought, why not just set the object as a variable on the request class, making it accessible to the controller?
It works, but I feel dirty? Is there a more appropriate way to handle this?
Ex.
Request Class
class DeleteCommentRequest extends Request {
var $comment = null;
public function authorize() {
$this->comment = comment::find(Input::get('comment_id'));
$user = Auth::user();
if($this->comment->user == $user)
return true;
return false;
}
public function rules() {
return [
'comment_id' => 'required|exists:recipes_comments,id'
];
}
}
Ex. Controller:
public function postDeleteComment(DeleteCommentRequest $request) {
$comment = $request->comment;
$comment->delete();
return $comment;
}
So what is my question? How do I best handle having to use the object twice when using the new Laravel 5 requests? Am I possibly overextending the functionality of the application? Is it ok to store the object in the application class so I can reach it later in my controller?
I would require ownership on the query itself and then check if the collection is empty.
class DeleteCommentRequest extends Request {
var $comment = null;
public function authorize() {
$this->comment = comment::where('id',Input::get('comment_id'))->where('user_id',Auth::id())->first();
if($this->comment->is_empty())
return false;
return true;
}
public function rules() {
return [
'comment_id' => 'required|exists:recipes_comments,id'
];
}
}
Since you're wanting to use the Model in two different places, but only query it once I would recommenced you use route-model binding.
In your RouteServiceProvider class (or any relevant provider) you'll want to bind the comment query from inside the boot method. The first parameter of bind() will be value that matches the wildcard in your route.
public function boot()
{
app()->router->bind( 'comment_id', function ($comment_id) {
return comment::where('id',$comment_id)->where('user_id',Auth::id())->first();
} );
}
Once that's set up you can access the Model from your DeleteCommentRequest like so
$this->comment_id
Note: The variable is Comment_id because that's what matches your route, but it will contain the actual model.
From your controller you just inject it like so
public function postDeleteComment(Comment $comment, DeleteCommentRequest $request) {
$comment->delete();
return $comment;
}

Categories