How can I create group routing in PHP with Closure? I'm creating my own REST API from scratch in PHP for practice and learning.
In bootstrap file i call App class:
$app = new App/App();
$app->import('routes.php');
I have routes.php file with:
$app->group('/api/v1', function() use ($app)
{
$app->group('/users', function() use ($app)
{
$app->get('/', 'User', 'index');
$app->post('/', 'User', 'post');
$app->get('/{id}', 'User', 'get');
$app->put('/{id}', 'User', 'put');
$app->delete('/{id}', 'User', 'delete');
});
});
It needs to create routes like this:
/api/v1/users/
/api/v1/users/
/api/v1/users/{id}
/api/v1/users/{id}
/api/v1/users/{id}
App class:
class App
{
public function group($link, Closure $closure)
{
$closure();
}
}
And it sets routes like this:
/
/
/{id}
/{id}
/{id}
What should I do to prefix urls ? How can I "foreach" these other $app->get(), $app->post() method callings ?
Figured it out! Added DI container to App class which handles Router, Route and RouteGroup classes. PHP SLIM framework was my inspiration - https://github.com/slimphp/Slim/tree/3.x/Slim
First I call group() method from App class with calls pushGroup() method from Router class. Then I invoke RouteGroup class with $group();. After that I cal popGroup() to return only last route group.
When adding groups url to Route, just run processGroups() method in Router class to add prefix links.
App class
/**
* Route groups
*
* #param string $link
* #param Closure $closure
* #return void
*/
public function group($link, Closure $closure)
{
$group = $this->container->get('Router')->pushGroup($link, $closure);
$group();
$this->container->get('Router')->popGroup();
}
Router class
/**
* Process route groups
*
* #return string
*/
private function processGroups()
{
$link = '';
foreach ($this->route_groups as $group) {
$link .= $group->getUrl();
}
return $link;
}
/**
* Add a route group to the array
* #param string $link
* #param Closure $closure
* #return RouteGroup
*/
public function pushGroup($link, Closure $closure)
{
$group = new RouteGroup($link, $closure);
array_push($this->route_groups, $group);
return $group;
}
/**
* Removes the last route group from the array
*
* #return RouteGroup|bool The RouteGroup if successful, else False
*/
public function popGroup()
{
$group = array_pop($this->route_groups);
return ($group instanceof RouteGroup ? $group : false);
}
Route class is basic class with routing parameters - method, url, controller, action and additional parameters so I won't copy it here.
RouteGroup class
/**
* Create a new RouteGroup
*
* #param string $url
* #param Closure $closure
*/
public function __construct($url, $closure)
{
$this->url = $url;
$this->closure = $closure;
}
/**
* Invoke the group to register any Routable objects within it.
*
* #param Slinky $app The App to bind the callable to.
*/
public function __invoke()
{
$closure = $this->closure;
$closure();
}
Related
I need some clarification with dynamic controller method
i upgrading the laravel 5.1 to 8.*, all is done, but only one bug,
my url is admin/admin-profile in 5.1 is working fine, but laravel 8 is not working 404 page error is showing.
this url will call method getAdminProfile(){ } but is not calling.
if this functionality is not available in laravel 8, then how can i manage this, if single url i will create route, but my application have more than 100 url like this, so please help me to solve this...
i was check this issues by compare all file both laravel 5.1 and laravel 8
missing one file to capture the like this problem from ControllerInspector from routing folder.
so please help to solve this..
i can't write each method in web.php
Route::controller('admin', 'AdminController');
class AdminController extends BaseController {
public function getIndex()
{
//
}
public function getAdminProfile()
{
//
}
public function anyLogin()
{
//
}
}
Resource Controller
If Resource Controller can handle your need so use that:
Documentation: https://laravel.com/docs/8.x/controllers#resource-controllers
and it's like below:
Routing:
use App\Http\Controllers\PhotoController;
Route::resource('photos', PhotoController::class);
----------------
If Resource Controller is not what you want:
you can get url and transfer url to controller function, for example:
router file getting any url after admin\
Route::prefix('admin')->group(function () {
Route::get('/{my_url?}', [AdminControlle:class, 'handle'])->where('my_url', '(.*)');
});
AdminController
public function handle(){
// get url segments after admin/ - if url is like your_domain.com/admin/...
$segments = array_slice(request()->segments(), 1);
// Translate array of segments to function name - implement by you
$functionName='';
// calling function with knowing its name
$functionName() // or can use call_user_func($functionName);
}
*Note: be aware that this dynamic route handling doesn't provide a solution for dynamic handling of http methods (post, get, patch, ...).
in this example i used GET method.
I just add below code in router.php
/**
* Register an array of controllers with wildcard routing.
*
* #param array $controllers
* #return void
*
* #deprecated since version 5.1.
/
public function controllers(array $controllers)
{
foreach ($controllers as $uri => $controller) {
$this->controller($uri, $controller);
}
}
/*
* Prepend the last group uses onto the use clause.
*
* #param string $uses
* #return string
*/
protected function prependGroupUses($uses)
{
$group = end($this->groupStack);
return isset($group['namespace']) && strpos($uses, '\\') !== 0 ? $group['namespace'].'\\'.$uses : $uses;
}
/**
* Route a controller to a URI with wildcard routing.
*
* #param string $uri
* #param string $controller
* #param array $names
* #return void
*
* #deprecated since version 5.1.
*/
public function controller($uri, $controller, $names = [])
{
$prepended = $controller;
// First, we will check to see if a controller prefix has been registered in
// the route group. If it has, we will need to prefix it before trying to
// reflect into the class instance and pull out the method for routing.
if (! empty($this->groupStack)) {
$prepended = $this->prependGroupUses($controller);
}
$routable = (new ControllerInspector)
->getRoutable($prepended, $uri);
// When a controller is routed using this method, we use Reflection to parse
// out all of the routable methods for the controller, then register each
// route explicitly for the developers, so reverse routing is possible.
// print_r($routable);
foreach ($routable as $method => $routes) {
foreach ($routes as $route) {
$this->registerInspected($route, $controller, $method, $names);
}
}
$this->addFallthroughRoute($controller, $uri);
}
/**
* Register an inspected controller route.
*
* #param array $route
* #param string $controller
* #param string $method
* #param array $names
* #return void
*
* #deprecated since version 5.1.
*/
protected function registerInspected($route, $controller, $method, &$names)
{
$action = ['uses' => $controller.'#'.$method];
// If a given controller method has been named, we will assign the name to the
// controller action array, which provides for a short-cut to method naming
// so you don't have to define an individual route for these controllers.
$action['as'] = Arr::get($names, $method);
$this->{$route['verb']}($route['uri'], $action);
}
/**
* Add a fallthrough route for a controller.
*
* #param string $controller
* #param string $uri
* #return void
*
* #deprecated since version 5.1.
*/
protected function addFallthroughRoute($controller, $uri)
{
$missing = $this->any($uri.'/{_missing}', $controller.'#missingMethod');
$missing->where('_missing', '(.*)');
}
Now is working normally
I am not sure the title of the question is clear, so I will try to explain it in details.
I want to execute a piece of code in every controller automatically, assign the result to a variable that will be globally accessible everywhere.
So the code that need to be run will be like this:
function getLanguage() {
session('lang') ? session('lang') : 'en';
}
$LANG = getLanguage();
In any controller I need to access that variable like this:
myModel::all($LANG);
Also in the view, it would be helpful to access that variable as well (if possible)
<div> User language: {{$LANG}}</div>
Is there any place where I can execute that piece of code automatically?
Create a middleware
Add new middleware to App\Http\Kernels $middleware property, if you want it to run on every request. You may also put into $middlewareGroups property's web key.
Your middleware's handle method will be like this
public function handle(Request $request, Closure $next)
{
Config::set('some-name.some-sub-name', session('lang') ?: 'en');
return $next($request);
}
You will be updating a config in your middleware. This config has to be set only in this middleware to prevent possible problems of shared global state. (it is also important to be unique)
Then you can use it with config('some-name.some-sub-name')
In your use-case, you should implement a global middleware which sets the locale as you wish
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Session\SessionManager;
use Illuminate\Contracts\Foundation\Application;
class CheckLocale
{
/**
* The application instance.
*
* #var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
/**
* The session manager instance.
*
* #var \Illuminate\Session\SessionManager
*/
protected $sessionManager;
public function __construct(Application $app, SessionManager $sessionManager)
{
$this->app = $app;
$this->sessionManager = $sessionManager;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
$this->app->setLocale($this->sessionManager->get('lang', 'en'));
return $next($request);
}
}
After setting it as a global middleware, you can access it wherever you need it from a controller or view
Controller
public function foo(Application $app)
{
$lang = $app->getLocale();
}
In a Blade view
#inject('app', Illuminate\Contracts\Foundation\Application::class)
{{ $app->getLocale() }}
For any other variable, you may directly use Laravel container
In a service provider register method:
$this->app->singleton('lang', function ($app) {
return $app['session']->get('lang', 'en');
});
And wherever else
app('lang');
Currently I am working on a project where we are trying to create a RESTful API. This API uses some default classes, for example the ResourceController, for basic behaviour that can be overwritten when needed.
Lets say we have an API resource route:
Route::apiResource('posts', 'ResourceController');
This route will make use of the ResourceController:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Repositories\ResourceRepository;
class ResourceController extends Controller
{
/**
* The resource class.
*
* #var string
*/
private $resourceClass = '\\App\\Http\\Resources\\ResourceResource';
/**
* The resource model class.
*
* #var string
*/
private $resourceModelClass;
/**
* The repository.
*
* #var \App\Repositories\ResourceRepository
*/
private $repository;
/**
* ResourceController constructor.
*
* #param \Illuminate\Http\Request $request
* #return void
*/
public function __construct(Request $request)
{
$this->resourceModelClass = $this->getResourceModelClass($request);
$this->repository = new ResourceRepository($this->resourceModelClass);
$exploded = explode('\\', $this->resourceModelClass);
$resourceModelClassName = array_last($exploded);
if (!empty($resourceModelClassName)) {
$resourceClass = '\\App\\Http\\Resources\\' . $resourceModelClassName . 'Resource';
if (class_exists($resourceClass)) {
$this->resourceClass = $resourceClass;
}
}
}
...
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate($request, $this->getResourceModelRules());
$resource = $this->repository->create($request->all());
$resource = new $this->resourceClass($resource);
return response()->json($resource);
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
$resource = $this->repository->show($id);
$resource = new $this->resourceClass($resource);
return response()->json($resource);
}
...
/**
* Get the model class of the specified resource.
*
* #param \Illuminate\Http\Request $request
* #return string
*/
private function getResourceModelClass(Request $request)
{
if (is_null($request->route())) return '';
$uri = $request->route()->uri;
$exploded = explode('/', $uri);
$class = str_singular($exploded[1]);
return '\\App\\Models\\' . ucfirst($class);
}
/**
* Get the model rules of the specified resource.
*
* #param \Illuminate\Http\Request $request
* #return string
*/
private function getResourceModelRules()
{
$rules = [];
if (method_exists($this->resourceModelClass, 'rules')) {
$rules = $this->resourceModelClass::rules();
}
return $rules;
}
}
As you can maybe tell we are not making use of model route binding and we make use of a repository to do our logic.
As you can also see we make use of some dirty logic, getResourceModelClass(), to determine the model class needed to perform logic on/with. This method is not really flexible and puts limits on the directory structure of the application (very nasty).
A solution could be adding some information about the model class when registrating the route. This could look like:
Route::apiResource('posts', 'ResourceController', [
'modelClass' => Post::class
]);
However it looks like this is not possible.
Does anybody have any suggestions on how to make this work or how to make our logic more clean and flexible. Flexibility and easy of use are important factors.
The nicest way would be to refactor the ResourceController into an abstract class and have a separate controller that extends it - for each resource.
I'm pretty sure that there is no way of passing some context information in routes file.
But you could bind different instances of repositories to your controller. This is generally a good practice, but relying on URL to resolve it is very hacky.
You'd have to put all the dependencies in the constructor:
public function __construct(string $modelPath, ResourceRepository $repo // ...)
{
$this->resourceModelClass = $this->modelPath;
$this->repository = $repo;
// ...
}
And do this in a service provider:
use App\Repositories\ResourceRepository;
use App\Http\Controllers\ResourceController;
// ... model imports
// ...
public function boot()
{
if (request()->path() === 'posts') {
$this->app->bind(ResourceRepository::class, function ($app) {
return new ResourceRepository(new Post);
});
$this->app->when(ResourceController::class)
->needs('$modelPath')
->give(Post::class);
} else if (request()->path() === 'somethingelse') {
// ...
}
}
This will give you more flexibility, but again, relying on pure URL paths is hacky.
I just showed an example for binding the model path and binding a Repo instance, but if you go down this road, you'll want to move all the instantiating out of the Controller constructor.
After a lot of searching and diving in the source code of Laravel I found out the getResourceAction method in the ResourceRegistrar handles the option passed to the route.
Further searching led me to this post where someone else already managed to extend this registrar en add some custom functionality.
My custom registrar looks like:
<?php
namespace App\Http\Routing;
use Illuminate\Routing\ResourceRegistrar as IlluResourceRegistrar;
class ResourceRegistrar extends IlluResourceRegistrar
{
/**
* Get the action array for a resource route.
*
* #param string $resource
* #param string $controller
* #param string $method
* #param array $options
* #return array
*/
protected function getResourceAction($resource, $controller, $method, $options)
{
$action = parent::getResourceAction($resource, $controller, $method, $options);
if (isset($options['model'])) {
$action['model'] = $options['model'];
}
return $action;
}
}
Do not forget to bind in the AppServiceProvider:
$registrar = new ResourceRegistrar($this->app['router']);
$this->app->bind('Illuminate\Routing\ResourceRegistrar', function () use ($registrar) {
return $registrar;
});
This custom registrar allows the following:
Route::apiResource('posts', 'ResourceController', [
'model' => Post::class
]);
And finally we are able to get our model class:
$resourceModelClass = $request->route()->getAction('model');
No hacky url parse logic anymore!
I have this route
Route::group(['middleware' => 'returnphase'], function () {
Route::get('/', 'FrontendController#home')->name('homepage');
});
My middleware check in what Phase (logic is non important now) is my application, i need that the controller setting up a global variable that i can use in all methods inside FrontendController because i need to read from database some data that depend from that check:
Middleware code, i need to set a phase_id varibale that i can use in may frontend controller.
namespace Cbcc\Http\Middleware;
use Closure;
class ReturnPhaseMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
/**
* TODO: Phase id check logic
*/
// SETTING GLOBAL PHASE ID VARIABLE (EXAMPLE PHASE_ID = 1)
return $next($request);
}
}
My frontend controller
//FrontEndController
namespace Cbcc\Http\Controllers;
use Cbcc\Page;
use Illuminate\Http\Request;
class FrontendController extends Controller
{
public function home()
{
$page = Page::where([
['phase_id',/**** I NEED GLOBAL PHASE ID HERE SETTING BY MIDDLEWARE***/],
['type','home']
])->get()[0];
return view('frontend.index',compact('page'));
}
}
Any ideas to do that?
You might want to user Session variables. What about using flash session variables since it seems you only use this variable once :
Middleware
class ReturnPhaseMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
/**
* TODO: Phase id check logic
*/
$request->session()->flash('PHASE_ID', '1');
return $next($request);
}
}
Front-end controller
class FrontendController extends Controller
{
public function home(Request $request)
{
$page = Page::where([
['phase_id', $request->session()->get('PHASE_Id')],
['type','home']
])->get()[0];
return view('frontend.index',compact('page'));
}
}
The special property of flash session variables is that they get destroyed at the next request.
Reference : https://laravel.com/docs/5.4/session#flash-data
Good strategy, and without using session?
What do you think about my solution?
// Frontend Controlller
namespace Cbcc\Http\Controllers;
use Cbcc\Lib\CheckPhaseInterface;
use Cbcc\Page;
class FrontendController extends Controller
{
protected
$phase_id;
public function __construct()
{
$this->middleware(function ($request, $next) {
// this 2 lines return phase id logic, for example $check->run() return 1 (int)
$check = resolve(CheckPhaseInterface::class);
$this->phase_id = $check->run();
return $next($request);
});
}
public function home()
{
$page = $this->getPageContent();
return view('frontend.index',compact('page'));
}
protected function getPageContent()
{
return Page::where([
['phase_id',$this->phase_id],
['type','home']
])->get()[0];
}
}
I'm trying to unit test a controller, but can't figure out how to pass some extra parameters to the routeMatch object.
I followed the posts from tomoram at http://devblog.x2k.co.uk/unit-testing-a-zend-framework-2-controller/ and http://devblog.x2k.co.uk/getting-the-servicemanager-into-the-test-environment-and-dependency-injection/, but when I try to dispatch a request to /album/edit/1, for instance, it throws the following exception:
Zend\Mvc\Exception\DomainException: Url plugin requires that controller event compose a router; none found
Here is my PHPUnit Bootstrap:
class Bootstrap
{
static $serviceManager;
static $di;
static public function go()
{
include 'init_autoloader.php';
$config = include 'config/application.config.php';
// append some testing configuration
$config['module_listener_options']['config_static_paths'] = array(getcwd() . '/config/test.config.php');
// append some module-specific testing configuration
if (file_exists(__DIR__ . '/config/test.config.php')) {
$moduleConfig = include __DIR__ . '/config/test.config.php';
array_unshift($config['module_listener_options']['config_static_paths'], $moduleConfig);
}
$serviceManager = Application::init($config)->getServiceManager();
self::$serviceManager = $serviceManager;
// Setup Di
$di = new Di();
$di->instanceManager()->addTypePreference('Zend\ServiceManager\ServiceLocatorInterface', 'Zend\ServiceManager\ServiceManager');
$di->instanceManager()->addTypePreference('Zend\EventManager\EventManagerInterface', 'Zend\EventManager\EventManager');
$di->instanceManager()->addTypePreference('Zend\EventManager\SharedEventManagerInterface', 'Zend\EventManager\SharedEventManager');
self::$di = $di;
}
static public function getServiceManager()
{
return self::$serviceManager;
}
static public function getDi()
{
return self::$di;
}
}
Bootstrap::go();
Basically, we are creating a Zend\Mvc\Application environment.
My PHPUnit_Framework_TestCase is enclosed in a custom class, which goes like this:
abstract class ControllerTestCase extends TestCase
{
/**
* The ActionController we are testing
*
* #var Zend\Mvc\Controller\AbstractActionController
*/
protected $controller;
/**
* A request object
*
* #var Zend\Http\Request
*/
protected $request;
/**
* A response object
*
* #var Zend\Http\Response
*/
protected $response;
/**
* The matched route for the controller
*
* #var Zend\Mvc\Router\RouteMatch
*/
protected $routeMatch;
/**
* An MVC event to be assigned to the controller
*
* #var Zend\Mvc\MvcEvent
*/
protected $event;
/**
* The Controller fully qualified domain name, so each ControllerTestCase can create an instance
* of the tested controller
*
* #var string
*/
protected $controllerFQDN;
/**
* The route to the controller, as defined in the configuration files
*
* #var string
*/
protected $controllerRoute;
public function setup()
{
parent::setup();
$di = \Bootstrap::getDi();
// Create a Controller and set some properties
$this->controller = $di->newInstance($this->controllerFQDN);
$this->request = new Request();
$this->routeMatch = new RouteMatch(array('controller' => $this->controllerRoute));
$this->event = new MvcEvent();
$this->event->setRouteMatch($this->routeMatch);
$this->controller->setEvent($this->event);
$this->controller->setServiceLocator(\Bootstrap::getServiceManager());
}
public function tearDown()
{
parent::tearDown();
unset($this->controller);
unset($this->request);
unset($this->routeMatch);
unset($this->event);
}
}
And we create a Controller instance and a Request with a RouteMatch.
The code for the test:
public function testEditActionWithGetRequest()
{
// Dispatch the edit action
$this->routeMatch->setParam('action', 'edit');
$this->routeMatch->setParam('id', $album->id);
$result = $this->controller->dispatch($this->request, $this->response);
// rest of the code isn't executed
}
I'm not sure what I'm missing here. Can it be any configuration for the testing bootstrap? Or should I pass the parameters in some other way? Or am I forgetting to instantiate something?
What I did to solve this problem was move the Application::init() and the configuration from the Bootstrap to the setUp() method. It takes a while to load, but it works.
My Bootstrap has only the code needed to configure the autoloader, while the setUp() method has something similar to the old Bootstrap::go() code.