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.
Related
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 a strange error with Twig and the WebProfiler when I enable a Doctrine filter.
request.CRITICAL: Uncaught PHP Exception Twig_Error_Runtime: "An exception has been thrown
during the rendering of a template ("Error when rendering "http://community.localhost:8000/
_profiler/e94abf?community_subdomain=community&panel=request" (Status code is 404).")." at
/../vendor/symfony/symfony/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/
layout.html.twig line 103
This {{ render(path('_profiler_search_bar', request.query.all)) }} causes the error.
My doctrine filter allows to add filter constraint on some classes (multi tenant app with dynamic subdomains)
<?php
namespace AppBundle\Group\Community;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
/**
* Class CommunityAwareFilter
*/
class CommunityAwareFilter extends SQLFilter
{
/**
* Gets the SQL query part to add to a query.
*
* #param ClassMetadata $targetEntity
* #param string $targetTableAlias
*
* #return string The constraint SQL if there is available, empty string otherwise.
*/
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if (!$targetEntity->reflClass->implementsInterface(CommunityAwareInterface::class)) {
return '';
}
return sprintf('%s.community_id = %s', $targetTableAlias, $this->getParameter('communityId')); // <-- error
// return ''; <-- no error
}
}
I have also extended Symfony Router to add subdomain placeholder automatically in routing.
Do you have any idea what can cause this ?
UPDATE
<?php
namespace AppBundle\Routing;
use AppBundle\Group\Community\CommunityResolver;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router as BaseRouter;
class Router implements RouterInterface
{
/**
* #var BaseRouter
*/
private $router;
/**
* #var RequestStack
*/
private $request;
/**
* #var CommunityResolver
*/
private $communityResolver;
/**
* Router constructor.
*
* #param BaseRouter $router
* #param RequestStack $request
* #param CommunityResolver $communityResolver
*/
public function __construct(BaseRouter $router, RequestStack $request, CommunityResolver $communityResolver)
{
$this->router = $router;
$this->request = $request;
$this->communityResolver = $communityResolver;
}
/**
* Sets the request context.
*
* #param RequestContext $context The context
*/
public function setContext(RequestContext $context)
{
$this->router->setContext($context);
}
/**
* Gets the request context.
*
* #return RequestContext The context
*/
public function getContext()
{
return $this->router->getContext();
}
/**
* Gets the RouteCollection instance associated with this Router.
*
* #return RouteCollection A RouteCollection instance
*/
public function getRouteCollection()
{
return $this->router->getRouteCollection();
}
/**
* Tries to match a URL path with a set of routes.
*
* If the matcher can not find information, it must throw one of the exceptions documented
* below.
*
* #param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded)
*
* #return array An array of parameters
*
* #throws ResourceNotFoundException If the resource could not be found
* #throws MethodNotAllowedException If the resource was found but the request method is not allowed
*/
public function match($pathinfo)
{
return $this->router->match($pathinfo);
}
public function generate($name, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
{
if (null !== ($community = $this->communityResolver->getCommunity())) {
$parameters['community_subdomain'] = $community->getSubDomain();
}
return $this->router->generate($name, $parameters, $referenceType);
}
}
I found the solution, in fact I passed my "tenant" (here my "community") object in the Session like this (in a subscriber onKernelRequest)
if (null === ($session = $request->getSession())) {
$session = new Session();
$session->start();
$request->setSession($session);
}
$session->set('community', $community);
I changed to store this object in a service and it works. Maybe using the Session to store data is a bad practice.
I think your Symmfony Router override may cause the problem. Can you paste us the code ?
I am using Nicolas Widart's Laravel Modules package to help manage a large app, and keep everything separated into logical modules. I would like to be able to drop in different modules and have them play nicely without any extra configuration.
All of my modules will define interfaces and default implementations that allow the application (the system controlling which modules are loaded) to specify that it wants to use a specific implementation instead, through dependency injection.
I am able to make some assumptions by having some modules require others, for example a payment processing module (Module PP) can assume that a payment is tied to a user (with which the interface for a user is defined in another module, Module U).
My ideal scenario is that I could add to an existing PHP interface that is defined in another required module. For example, being able to retrieve a user from a repository defined in Module U and call a method on it that was defined in Module PP.
Once Module PP resolves the interface (again, through dependency injection) from Module U to a class, I want my method from Module PP to be callable on that class.
I have been able to achieve this using the __call magic method as below.
Extensions Module
This module defines the core operations to add to an existing interface.
IsExtendable Interface
<?php
namespace Modules\Extensions\Contracts;
interface IsExtendable
{
/**
* Get the list of extensions for this entity.
*
* #return array
*/
public static function getExtensions();
/**
* Adds an extension to this entity.
*
* #param string $name
* #param mixed $function
*/
public static function addExtension($name, $function);
/**
* Checks whether the entity has the given extension.
*
* #param string $name
*
* #return bool
*/
public static function hasExtension($name);
/**
* Call the extension if it exists, or pass it further up the chain.
*
* #param string $name
* #param mixed $arguments
*
* #return mixed
*/
public function __call($name, $arguments);
}
IsExtendable Trait
<?php
namespace Modules\Extensions;
trait IsExtendable
{
/** #var $extensions */
private static $extensions = [];
/**
* Get the list of extensions for this entity.
*
* #return array
*/
public static function getExtensions()
{
return self::$extensions;
}
/**
* Adds an extension to this entity.
*
* #param string $name
* #param mixed $function
*/
public static function addExtension($name, $function)
{
if(is_callable($function) == FALSE)
{
throw new \InvalidArgumentException('Function must be callable.');
}
self::$extensions[$name] = $function;
}
/**
* Checks whether the entity has the given extension.
*
* #param string $name
*
* #return bool
*/
public static function hasExtension($name)
{
return array_key_exists($name, self::getExtensions()) == TRUE;
}
/**
* Calls the extension if it exists, or passes it further up the chain.
*
* #param string $name
* #param mixed $arguments
*
* #return mixed
*/
public function __call($name, $arguments)
{
if(self::hasExtension($name) == TRUE)
{
$callable = self::getExtensions()[$name];
return call_user_func_array($callable, array_merge(array($this), $arguments));
}
else
{
return parent::__call($name, $arguments);
}
}
}
Service Provider
<?php
namespace Modules\Extensions\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Extensions\Contracts\IsExtendable as IsExtendableContract;
class ExtensionServiceProvider extends ServiceProvider
{
/**
* #param string $implementation
* #param string $functionName
*
* #return callable
*/
public function prepareExtension($implementation, $functionName)
{
return $implementation . '::' . $functionName;
}
/**
* #param string $contract
* #param string $implementation
*
* #return void
*/
public function extractExtensions($contract, $implementation)
{
$reflection = new \ReflectionClass($implementation);
$methods = [];
foreach($reflection->getMethods(\ReflectionMethod::IS_STATIC) as $method)
{
// TODO: May be able to use $method->getClosure() here
// https://stackoverflow.com/questions/8299886/php-get-static-methods
$methods[] = $method->getName();
}
$this->registerExtensions($contract, $methods, $implementation);
}
/**
* #param string $contract
* #param string $name
* #param string $function
*
* #return void
*/
public function registerExtension($contract, $name, $function)
{
// Resolve the contract to an implementation
$base = app($contract);
// Check that it is suitable for extension
if($base instanceof IsExtendableContract)
{
$base::addExtension($name, $function);
}
}
/**
* #param string $contract
* #param array $extensions
* #param string|null $implementation
*
* #return void
*/
public function registerExtensions($contract, array $extensions = [], $implementation = NULL)
{
// Resolve the contract to an implementation
$base = app($contract);
// Check that it is suitable for extension
if($base instanceof IsExtendableContract)
{
foreach($extensions as $name => $function)
{
if(is_int($name) == TRUE)
{
if(is_string($function) == TRUE)
{
$name = $function;
}
else
{
throw new \InvalidArgumentException('All extensions must have a valid name.');
}
}
if(is_string($function) == TRUE)
{
if(strpos($function, '::') === FALSE && $implementation != NULL)
{
$function = $this->prepareExtension($implementation, $function);
}
}
$base::addExtension($name, $function);
}
}
}
}
Module U
User Interface
<?php
namespace Modules\Auth\Contracts\Entities;
interface User
{
/**
* #return int
*/
public function getId();
/**
* #return string
*/
public function getName();
/**
* #return string
*/
public function getEmail();
/**
* #return \DateTime
*/
public function getCreatedAt();
/**
* #return \DateTime
*/
public function getUpdatedAt();
}
User Implementation
<?php
namespace Modules\Auth\Entities;
use Modules\Extensions\Contracts\IsExtendable as IsExtendableContract;
use Modules\Auth\Contracts\Entities\User as UserContract;
use Modules\Extensions\IsExtendable;
class User implements
IsExtendableContract,
UserContract
{
use IsExtendable;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #return string
*/
public function getEmail()
{
return $this->email;
}
/**
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->created_at;
}
/**
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updated_at;
}
}
Module PP
User Extension
<?php
namespace Modules\Test\Entities\Extensions;
use Modules\Auth\Contracts\Entities\User;
class UserExtension
{
/**
* #param User $context
*/
public static function getCardLastFour($context)
{
return $context->card_last_four;
}
/**
* #param User $context
*/
public static function getCardBrand($context)
{
return $context->card_brand;
}
/**
* #param User $context
*/
public static function getStripeId($context)
{
return $context->stripe_id;
}
}
Service Provider
<?php
namespace Modules\Test\Providers\Extensions;
use Modules\Auth\Contracts\Entities\User as UserContract;
use Modules\Test\Entities\Extensions\UserExtension;
use Modules\Extensions\Providers\ExtensionServiceProvider;
class StripeExtensionProvider extends ExtensionServiceProvider
{
public function boot()
{
// TODO: Set the contract as a static field on the extension to then automatically extract from all extension files in a folder
$this->extractExtensions(UserContract::class, UserExtension::class);
}
}
My question is, is this method scalable (across maybe 10 modules), and can you foresee any issues with it? Or is there a better/more popular (and supported) way to do this? I don't want to get 2 years into a project and discover that I really hate the way I've implemented this.
I know that this concept won't support IDE autocompletion out of the box but I could build in a way to generate the PHPDocs similar to this package.
I have researched the Decorator pattern but this feels clunky in that I would always need to rely on a new implementation within each module, instead of just adding to the existing one.
I realise this is a big question so my sincere thanks to anyone willing to have a look at it!
Check out Laravel's macroable trait. It's basically the same idea, and Laravel uses it all over the place.
So yes, it scales - up to a certain point. Like almost everything else, this is a tool that can be abused. Use it with a dash of common sense, and you should be OK.
I'm writing my own implementation of the Laravel Service Container to practice some design patterns and later make a private microframework.
The class looks like this right now:
class Container implements ContainerInterface
{
/**
* Concrete bindings of contracts.
*
* #var array
*/
protected $bindings = [];
/**
* Lists of arguments used for a class instantiation.
*
* #var array
*/
protected $arguments = [];
/**
* Container's storage used to store already built or customly setted objects.
*
* #var array
*/
protected $storage = [];
/**
* Returns an instance of a service
*
* #param $name
* #return object
* #throws \ReflectionException
*/
public function get($name) {
$className = (isset($this->bindings[$name])) ? $this->bindings[$name] : $name;
if (isset($this->storage[$className])) {
return $this->storage[$className];
}
return $this->make($className);
}
/**
* Creates an instance of a class
*
* #param $className
* #return object
* #throws \ReflectionException
*/
public function make($className) {
$refObject = new \ReflectionClass($className);
if (!$refObject->isInstantiable()) {
throw new \ReflectionException("$className is not instantiable");
}
$refConstructor = $refObject->getConstructor();
$refParameters = ($refConstructor) ? $refConstructor->getParameters() : [];
$args = [];
// Iterates over constructor arguments, checks for custom defined parameters
// and builds $args array
foreach ($refParameters as $refParameter) {
$refClass = $refParameter->getClass();
$parameterName = $refParameter->name;
$parameterValue =
isset($this->arguments[$className][$parameterName]) ? $this->arguments[$className][$parameterName]
: (null !== $refClass ? $refClass->name
: ($refParameter->isOptional() ? $refParameter->getDefaultValue()
: null));
// Recursively gets needed objects for a class instantiation
$args[] = ($refClass) ? $this->get($parameterValue)
: $parameterValue;
}
$instance = $refObject->newInstanceArgs($args);
$this->storage[$className] = $instance;
return $instance;
}
/**
* Sets a concrete implementation of a contract
*
* #param $abstract
* #param $concrete
*/
public function bind($abstract, $concrete) {
$this->bindings[$abstract] = $concrete;
}
/**
* Sets arguments used for a class instantiation
*
* #param $className
* #param array $arguments
*/
public function setArguments($className, array $arguments) {
$this->arguments[$className] = $arguments;
}
}
It works fine but I clearly see a violation of SRP in the make() method. So I decided to delegate an object creational logic to a separate class.
A problem that I encountered is that this class will be tightly coupled with a Container class. Because it needs an access to $bindings and $arguments arrays, and the get() method. And even if we pass these parameters to the class, the storage still stays in a container. So basically all architecture is wrong and we need, like, 2 more classes: StorageManager and ClassFactory. Or maybe ClassBuilder? And should ClassFactory be able to build constructor arguments or it needs another class — ArgumentFactory?
What do you think guys?
I like clean docs and phpdoc will automagically look up the type. When documenting a controller function that returns View::make, I have no idea what type to use for the #return in my documentation.
<?php
class FooController extends BaseController {
/**
* Show a view.
*
* #return ??? description of the view
*/
public function show(){
return View::make('bar');
}
}
What is the type here or is there a better way to document the function for this purpose?
The return value is
Illuminate\View\View
I traced through the ServiceProvider which lead me to
Illuminate\View\Environment::make
Which is line 113 of vendor/laravel/framework/src/Illuminate/View/Environment.php (in 4.1 at least)
/**
* Get a evaluated view contents for the given view.
*
* #param string $view
* #param array $data
* #param array $mergeData
* #return \Illuminate\View\View
*/
public function make($view, $data = array(), $mergeData = array())
{
$path = $this->finder->find($view);
$data = array_merge($mergeData, $this->parseData($data));
$this->callCreator($view = new View($this, $this->getEngineFromPath($path), $view, $path, $data));
return $view;
}