Currently I have a REST route working for an Event controller (/event). I would like to handle Event SignUps in an EventSignUp controller, and map this controller to a /event/signups route.
The Zend Framework documentation states that the URL /event/signup/:id should map to an Event_SignupController. But this does not work for me.
I set up the default REST route for all controllers in my Bootstrap class:
$front = Zend_Controller_Front::getInstance();
$router = $front->getRouter();
// Specifying all controllers as RESTful:
$restRoute = new Zend_Rest_Route($front);
$router->addRoute('default', $restRoute);
Am I missing something or is the documentation just wrong? If the documentation is wrong, what approach should I take to achieve my desired goal?
As a side note, a lot of existing controllers rely on the default REST route, so it would be nice if there is a solution that does not require to implement new routes for all existing controllers.
Edit: The documentation states that /product/ratings will be translated to the Product_RatingsController, which means the RatingsController in the Products module. Because all my controllers are placed in the default module, my desired behavior is not supported by the Rest Route.
So this changes my question, is it possible to achieve my desired behavior without affecting the existing controllers dependency on the default Rest route? If so, how? And if not, what would be the best approach for me to take?
Based on the comments of Haim Evgi I created a controller plugin that adds Zend_Controller_Router_Route routes based on the request method. This is the code of that controller plugin:
class TW_Webservice_Controller_Plugin_RestRoutes extends Zend_Controller_Plugin_Abstract
{
/**
*
* #var Zend_Controller_Router_Interface
*/
public $router;
/**
* Setup Rest routes that are not handled by the default Zend_Rest_Route object.
*
* #param Zend_Controller_Request_Abstract $request
*/
public function routeStartup(Zend_Controller_Request_Abstract $request)
{
$front = Zend_Controller_Front::getInstance();
$this->router = $front->getRouter();
$method = strtolower($request->getMethod());
$restRoutes = array(
'/event/signup' => 'event-signup'
);
$this->addRoutes($method, $restRoutes);
}
/**
*
* #param string $method The request method
* #param array $restRoutes Router pattern => Controller name pairs
*/
public function addRoutes($method, $restRoutes)
{
foreach ($restRoutes as $routePattern => $controllerName) {
switch ($method) {
case "get":
$this->addGetRoutes($routePattern, $controllerName);
break;
case "post":
$this->addPostRoute($routePattern, $controllerName);
break;
case "put":
$this->addPutRoute($routePattern, $controllerName);
break;
case "delete";
$this->addDeleteRoute($routePattern, $controllerName);
break;
}
}
}
/**
*
* #param string $routePattern
* #param string $controllerName
*/
public function addGetRoutes($routePattern, $controllerName)
{
$this->addRestRoute($routePattern, $controllerName, 'index');
$routePattern = $routePattern . '/:id';
$this->addRestRoute($routePattern, $controllerName, 'get');
}
/**
*
* #param string $routePattern
* #param string $controllerName
*/
public function addPostRoute($routePattern, $controllerName)
{
$this->addRestRoute($routePattern, $controllerName, 'post');
}
/**
*
* #param string $routePattern
* #param string $controllerName
*/
public function addPutRoute($routePattern, $controllerName)
{
$routePattern = $routePattern . '/:id';
$this->addRestRoute($routePattern, $controllerName, 'put');
}
/**
*
* #param string $routePattern
* #param string $controllerName
*/
public function addDeleteRoute($routePattern, $controllerName)
{
$routePattern = $routePattern . '/:id';
$this->addRestRoute($routePattern, $controllerName, 'delete');
}
/**
*
* #param string $routePattern
* #param string $controllerName
* #param string $action
*/
public function addRestRoute($routePattern, $controllerName, $action)
{
$route = new Zend_Controller_Router_Route($routePattern, array(
'controller' => $controllerName,
'action' => $action
));
$this->router->addRoute($controllerName . '-' . $action, $route);
}
}
It would be nicer if the $restRoutes array is retrieved from a config file, but for now this works.
Related
I have created a wrapper for the Slim request in my application to have the ability to create some custom methods on the Request object.
class Request extends SlimRequest
{
/**
* Get authorization token.
*
* #return null|string
*/
public function getAuthToken(): ?string
{
return $this->getHeaderLine('FOOBAR-TOKEN');
}
/**
* Retrieves a route parameter.
*
* #param string $name
* #return string|null
*/
public function getRouteParam(string $name): ?string
{
return $this->getRoute()->getArgument($name);
}
/**
* Retrieves the route instance.
*
* #return Route
*/
public function getRoute(): Route
{
return $this->getAttribute('route');
}
}
My problem comes when trying to create unit test for this class. The way I have been testing requests is by using Slims build in environment mocks. The first function I added a header to the request which can be seen below, but I can't figure out how to add a Route object to the request
$request = Request::createFromEnvironment(Environment::mock());
$request = $request->withHeader('FOOBAR-TOKEN', 'superSafeExampleToken');
I tried creating the request with a request options but $this->getAttribute('route'); returns null
$requestOptions = [
'REQUEST_METHOD' => 'POST,
'REQUEST_URI' => '/foo/bar',
'QUERY_STRING' => http_build_query($requestParameters),
];
$environment = Environment::mock($requestOptions);
Okay so the solution was the following
public function testGetRouteParam()
{
$route = $route = new Route('GET', '/foo/{bar}', []);
$route->setArguments(['bar' => 1]);
$request = Request::createFromEnvironment(Environment::mock());
$request = $request->withAttribute('route', $route);
$this->assertEquals(1, $request->getRouteParam('bar'));
$this->assertNull($request->getRouteParam('baz'));
}
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!
In my Symfony 2.8 project I have an extension that adds some extra logic to the trans method:
parameters:
translator.class: MyBundle\Twig\TranslationExtension
The class looks like this:
namespace MyBundle\Twig\TranslationExtension;
use Symfony\Bundle\FrameworkBundle\Translation\Translator as BaseTranslator;
class TranslationExtension extends BaseTranslator
{
private $currentLocale;
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
$translation = parent::trans($id, $parameters, $domain, $locale);
// Some extra logic here
return $translation;
}
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
return parent::transChoice($id, $number, $parameters, $domain, $locale);
}
}
Now, I'm migrating to Symfony 3, where those class parameters are deprecated, but how can I implement this by overwriting the translator service?
Instead of extending, it would be better to decorate the translator service. Right now you overriding the class name, which will also override other bundles that want to decorate the service. And I see you made it an extension because of Twig, the original Twig {{ trans() }} filter will use the decorated service too.
services:
app.decorating_translator:
class: AppBundle\DecoratingTranslator
decorates: translator
arguments: ['#app.decorating_translator.inner'] # original translator
public: false
See documentation about decorating here: http://symfony.com/doc/current/service_container/service_decoration.html
Here is full working example how to decorate translator service in symfony 3 and replace parameter in all translated strings.
Decorate service in config:
# app/config/services.yml
app.decorating_translator:
class: AppBundle\Translation\Translator
decorates: translator
arguments:
- '#app.decorating_translator.inner'
# passing custom parameters
- {'%%app_name%%': '%app_name%', '%%PRETTY_ERROR%%': 'This is not nice:'}
public: false
Create new translator that reuses original translator and adds parameters defined in service config. The only new code is updateParameters() method and call to it:
# AppBundle/Translation/Translator.php
namespace AppBundle\Translation;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Component\Translation\TranslatorInterface;
class Translator implements TranslatorInterface, TranslatorBagInterface
{
/** #var TranslatorBagInterface|TranslatorInterface */
protected $translator;
/** #var array */
private $parameters;
/**
* #param TranslatorInterface|TranslatorBagInterface $translator
* #param array $parameters
*/
public function __construct($translator, $parameters)
{
$this->translator = $translator;
$this->parameters = $parameters;
}
/**
* #param string $id
* #param array $parameters
* #param null $domain
* #param null $locale
*
* #return string
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
{
$parameters = $this->updateParameters($parameters);
return $this->translator->trans($id, $parameters, $domain, $locale);
}
/**
* #param string $id
* #param int $number
* #param array $parameters
* #param null $domain
* #param null $locale
*
* #return string
*/
public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
{
$parameters = $this->updateParameters($parameters);
return $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
}
/**
* #param string $locale
*/
public function setLocale($locale)
{
$this->translator->setLocale($locale);
}
/**
* #return string
*/
public function getLocale()
{
return $this->translator->getLocale();
}
/**
* #param string|null $locale
*
* #return \Symfony\Component\Translation\MessageCatalogueInterface
*/
public function getCatalogue($locale = null)
{
return $this->translator->getCatalogue($locale);
}
/**
* #param array $parameters
*
* #return array
*/
protected function updateParameters($parameters)
{
return array_merge($this->parameters, $parameters);
}
}
Now every time you translate message %app_config% will replaced with parameter from config (e.g. parameters.yml) and %PRETTY_ERROR% will be replace with static string.
If needed it is possible to override same parameters when calling trans:
{{ 'layout.title.home'|trans({'%app_name%': 'Real App No. 1'}) }}
Read about service decoration here
#Aurelijus Rozenas:
add this in your Translator:
public function __call($method, $args)
{
return \call_user_func_array(array($this->translator, $method), $args);
}
I'm trying to make a custom redirect function. I have created a custom route function in a new file (helpers.php) that works fine:
if (! function_exists('cms_route')) {
/**
* Generate a URL to a named route with predefined cms path.
*
* #param string $name
* #param array $parameters
* #param bool $absolute
* #param \Illuminate\Routing\Route $route
* #return string
*/
function cms_route($name, $parameters = [], $absolute = true, $route = null)
{
return app('url')->route(config('constants.cms_path').'.'.$name, $parameters, $absolute, $route);
}
}
I'm trying to call this function with redirect()->cms_route('name') instead of redirect()->route('name')
So when the cms path is changed everything keeps working.
How would I accomplish this?
Added as quick fix:
if (! function_exists('cms_redirect')) {
/**
* Get an instance of the redirector.
*
* #param string $name
* #param array $parameters
* #param bool $absolute
* #param \Illuminate\Routing\Route $route
* #return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
*/
function cms_redirect($name, $parameters = [])
{
return redirect()->route(config('constants.cms_path').'.'.$name, $parameters);
}
}
I'm learning Zend Framework at the moment and came across the following syntax.
class Zend_Controller_Action_Helper_Redirector extends Zend_Controller_Action_Helper_Abstract
{
/**
* Perform a redirect to an action/controller/module with params
*
* #param string $action
* #param string $controller
* #param string $module
* #param array $params
* #return void
*/
public function gotoSimple($action, $controller = null, $module = null, array $params = array())
{
$this->setGotoSimple($action, $controller, $module, $params);
if ($this->getExit()) {
$this->redirectAndExit();
}
}
/**
* direct(): Perform helper when called as
* $this->_helper->redirector($action, $controller, $module, $params)
*
* #param string $action
* #param string $controller
* #param string $module
* #param array $params
* #return void
*/
public function direct($action, $controller = null, $module = null, array $params = array())
{
$this->gotoSimple($action, $controller, $module, $params);
}
}
In Zend Framework the direct() method in this class can be called using the following syntax:
$this->_helper->redirector('index','index');
Where redirector is an object(!) in the _helper object, which is inside the controller object, in which we call the method. The syntactic sugar here is that you can just pass parameters to the object instead of to the method, which we would write like so:
$this->_helper->redirector->gotoSimple('index','index');
..which is all fine and dandy ofcourse.
Here's my question: is this direct() method standard in OO PHP? Or is this functionality built into Zend Framework?
I can't find any documentation on this.
Thanks!
It is functionality build into Zend Framework.
The $_helpers property in the Controller instance holds an Action_HelperBroker instance. This instance implements PHP's magic __call method. When you call a method that does not exist on that instance, it will try to use the method name to fetch a helper of the same name and call direct() on it (if possible). See code below.
From Zend_Controller_Action
/**
* Helper Broker to assist in routing help requests to the proper object
*
* #var Zend_Controller_Action_HelperBroker
*/
protected $_helper = null;
From Zend_Controller_Action_HelperBroker
/**
* Method overloading
*
* #param string $method
* #param array $args
* #return mixed
* #throws Zend_Controller_Action_Exception if helper does not have a direct() method
*/
public function __call($method, $args)
{
$helper = $this->getHelper($method);
if (!method_exists($helper, 'direct')) {
require_once 'Zend/Controller/Action/Exception.php';
throw new Zend_Controller_Action_Exception('Helper "' . $method . '" does not support overloading via direct()');
}
return call_user_func_array(array($helper, 'direct'), $args);
}
The Helper Broker also implement the magic __get method, so when you try to access a property that does not exist, the broker will use the property name as an argument to getHelper()
/**
* Retrieve helper by name as object property
*
* #param string $name
* #return Zend_Controller_Action_Helper_Abstract
*/
public function __get($name)
{
return $this->getHelper($name);
}
Please be aware that magic methods are not meant as a replacement to a proper API. While you can use them as shown above, calling the more verbose
$this->_helper->getHelper('redirector')->gotoSimple('index','index');
is often the much faster alternative.