Currently I am trying to learn the Zend Framework and therefore I bought the book "Zend Framework in Action".
In chapter 3, a basic model and controller is introduced along with unit tests for both of them. The basic controller looks like this:
class IndexController extends Zend_Controller_Action
{
public function indexAction()
{
$this->view->title = 'Welcome';
$placesFinder = new Places();
$this->view->places = $placesFinder->fetchLatest();
}
}
Places is the model class that fetches the latest places from the database. What bugs me here: how should I test the IndexController in isolation? As the reference to the Places class is "hardcoded", I cant inject any stubs or mocks in IndexController.
What I would rather like to have is something like this:
class IndexController extends Zend_Controller_Action
{
private $placesFinder;
// Here I can inject anything: mock, stub, the real instance
public function setPlacesFinder($places)
{
$this->placesFinder = $places;
}
public function indexAction()
{
$this->view->title = 'Welcome';
$this->view->places = $this->placesFinder->fetchLatest();
}
}
The first code sample I posted is most definately NOT unit test friendly as IndexController cannot be tested in isolation. The second one is much better. Now I just need some way to inject the model instances into the controller objects.
I know that the Zend Framework per se has no component for dependency injection. But there are some good frameworks out there for PHP, can any be used together with Zend Framework? Or is there some other way to do this in Zend Framework?
Logic to models
First of all, it's worth to mention, that controllers should need only functional tests, though all the logic belongs to models.
My implementation
Here is an excerpt from my Action Controller implementation, which solves the following problems:
allows inject any dependency to actions
validates the action parameters, e.g. you may not pass array in $_GET when integer is expected
My full code allows also to generate canonical URL (for SEO or unique page hash for stats) based or required or handled action params. For this, I use this abstract Action Controller and custom Request object, but this is not the case we discuss here.
Obviously, I use Reflections to automatically determine action parameters and dependency objects.
This is a huge advantage and simplifies the code, but also has an impact in performance (minimal and not important in case of my app and server), but you may implement some caching to speed it up. Calculate the benefits and the drawbacks, then decide.
DocBlock annotations are becoming a pretty well known industry standard, and parsing it for evaluation purposes becomes more popular (e.g. Doctrine 2). I used this technique for many apps and it worked nicely.
Writing this class I was inspired by Actions, now with params! and Jani Hartikainen's blog post.
So, here is the code:
<?php
/**
* Enchanced action controller
*
* Map request parameters to action method
*
* Important:
* When you declare optional arguments with default parameters,
* they may not be perceded by optional arguments,
* e.g.
* #example
* indexAction($username = 'tom', $pageid); // wrong
* indexAction($pageid, $username = 'tom'); // OK
*
* Each argument must have #param DocBlock
* Order of #param DocBlocks *is* important
*
* Allows to inject object dependency on actions:
* #example
* * #param int $pageid
* * #param Default_Form_Test $form
* public function indexAction($pageid, Default_Form_Test $form = null)
*
*/
abstract class Your_Controller_Action extends Zend_Controller_Action
{
/**
*
* #var array
*/
protected $_basicTypes = array(
'int', 'integer', 'bool', 'boolean',
'string', 'array', 'object',
'double', 'float'
);
/**
* Detect whether dispatched action exists
*
* #param string $action
* #return bool
*/
protected function _hasAction($action)
{
if ($this->getInvokeArg('useCaseSensitiveActions')) {
trigger_error(
'Using case sensitive actions without word separators' .
'is deprecated; please do not rely on this "feature"'
);
return true;
}
if (method_exists($this, $action)) {
return true;
}
return false;
}
/**
*
* #param string $action
* #return array of Zend_Reflection_Parameter objects
*/
protected function _actionReflectionParams($action)
{
$reflMethod = new Zend_Reflection_Method($this, $action);
$parameters = $reflMethod->getParameters();
return $parameters;
}
/**
*
* #param Zend_Reflection_Parameter $parameter
* #return string
* #throws Your_Controller_Action_Exception when required #param is missing
*/
protected function _getParameterType(Zend_Reflection_Parameter $parameter)
{
// get parameter type
$reflClass = $parameter->getClass();
if ($reflClass instanceof Zend_Reflection_Class) {
$type = $reflClass->getName();
} else if ($parameter->isArray()) {
$type = 'array';
} else {
$type = $parameter->getType();
}
if (null === $type) {
throw new Your_Controller_Action_Exception(
sprintf(
"Required #param DocBlock not found for '%s'", $parameter->getName()
)
);
}
return $type;
}
/**
*
* #param Zend_Reflection_Parameter $parameter
* #return mixed
* #throws Your_Controller_Action_Exception when required argument is missing
*/
protected function _getParameterValue(Zend_Reflection_Parameter $parameter)
{
$name = $parameter->getName();
$requestValue = $this->getRequest()->getParam($name);
if (null !== $requestValue) {
$value = $requestValue;
} else if ($parameter->isDefaultValueAvailable()) {
$value = $parameter->getDefaultValue();
} else {
if (!$parameter->isOptional()) {
throw new Your_Controller_Action_Exception(
sprintf("Missing required value for argument: '%s'", $name));
}
$value = null;
}
return $value;
}
/**
*
* #param mixed $value
*/
protected function _fixValueType($value, $type)
{
if (in_array($type, $this->_basicTypes)) {
settype($value, $type);
}
return $value;
}
/**
* Dispatch the requested action
*
* #param string $action Method name of action
* #return void
*/
public function dispatch($action)
{
$request = $this->getRequest();
// Notify helpers of action preDispatch state
$this->_helper->notifyPreDispatch();
$this->preDispatch();
if ($request->isDispatched()) {
// preDispatch() didn't change the action, so we can continue
if ($this->_hasAction($action)) {
$requestArgs = array();
$dependencyObjects = array();
$requiredArgs = array();
foreach ($this->_actionReflectionParams($action) as $parameter) {
$type = $this->_getParameterType($parameter);
$name = $parameter->getName();
$value = $this->_getParameterValue($parameter);
if (!in_array($type, $this->_basicTypes)) {
if (!is_object($value)) {
$value = new $type($value);
}
$dependencyObjects[$name] = $value;
} else {
$value = $this->_fixValueType($value, $type);
$requestArgs[$name] = $value;
}
if (!$parameter->isOptional()) {
$requiredArgs[$name] = $value;
}
}
// handle canonical URLs here
$allArgs = array_merge($requestArgs, $dependencyObjects);
// dispatch the action with arguments
call_user_func_array(array($this, $action), $allArgs);
} else {
$this->__call($action, array());
}
$this->postDispatch();
}
$this->_helper->notifyPostDispatch();
}
}
To use this, just:
Your_FineController extends Your_Controller_Action {}
and provide annotations to actions, as usual (at least you already should ;).
e.g.
/**
* #param int $id Mandatory parameter
* #param string $sorting Not required parameter
* #param Your_Model_Name $model Optional dependency object
*/
public function indexAction($id, $sorting = null, Your_Model_Name $model = null)
{
// model has been already automatically instantiated if null
$entry = $model->getOneById($id, $sorting);
}
(DocBlock is required, however I use Netbeans IDE, so the DocBlock is automatically generated based on action arguments)
Ok, this is how I did it:
As IoC Framework I used this component of the symfony framework (but I didnt download the latest version, I used an older one I used on projects before... keep that in mind!). I added its classes under /library/ioc/lib/.
I added these init function in my Bootstrap.php in order to register the autoloader of the IoC framework:
protected function _initIocFrameworkAutoloader()
{
require_once(APPLICATION_PATH . '/../library/Ioc/lib/sfServiceContainerAutoloader.php');
sfServiceContainerAutoloader::register();
}
Next, I made some settings in application.ini which set the path to the wiring xml and allow to disable automatic dependency injection e. g. in unit tests:
ioc.controllers.wiringXml = APPLICATION_PATH "/objectconfiguration/controllers.xml"
ioc.controllers.enableIoc = 1
Then, I created a custom builder class, which extends sfServiceContainerBuilder and put it under /library/MyStuff/Ioc/Builder.php. In this test project I keep all my classes under /library/MyStuff/.
class MyStuff_Ioc_Builder extends sfServiceContainerBuilder
{
public function initializeServiceInstance($service)
{
$serviceClass = get_class($service);
$definition = $this->getServiceDefinition($serviceClass);
foreach ($definition->getMethodCalls() as $call)
{
call_user_func_array(array($service, $call[0]), $this->resolveServices($this->resolveValue($call[1])));
}
if ($callable = $definition->getConfigurator())
{
if (is_array($callable) && is_object($callable[0]) && $callable[0] instanceof sfServiceReference)
{
$callable[0] = $this->getService((string) $callable[0]);
}
elseif (is_array($callable))
{
$callable[0] = $this->resolveValue($callable[0]);
}
if (!is_callable($callable))
{
throw new InvalidArgumentException(sprintf('The configure callable for class "%s" is not a callable.', get_class($service)));
}
call_user_func($callable, $service);
}
}
}
Last, I created a custom controller class in /library/MyStuff/Controller.php which all my controllers inherit from:
class MyStuff_Controller extends Zend_Controller_Action {
/**
* #override
*/
public function dispatch($action)
{
// NOTE: the application settings have to be saved
// in the registry with key "config"
$config = Zend_Registry::get('config');
if($config['ioc']['controllers']['enableIoc'])
{
$sc = new MyStuff_Ioc_Builder();
$loader = new sfServiceContainerLoaderFileXml($sc);
$loader->load($config['ioc']['controllers']['wiringXml']);
$sc->initializeServiceInstance($this);
}
parent::dispatch($action);
}
}
What this basically does is using the IoC Framework in order to initialize the already created controller instance ($this). Simple tests I did seemed to do what I want... let´s see how this performs in real life situations. ;)
It´s still monkey patching somehow, but the Zend Framework doesn´t seem to provide a hook where I can create the controller instance with a custom controller factory, so this is the best I came up with...
I'm currently working on the same question, and after deep research I've decide to use Symfony Dependency Injection component. You can get good info from official website http://symfony.com/doc/current/book/service_container.html.
I've build custom getContainer() method in bootstrap, which resturns now service container, and it simply can be used in controllers like
public function init()
{
$sc = $this->getInvokeArg('bootstrap')->getContainer();
$this->placesService = $sc->get('PlacesService');
}
Here you can find how to do that http://blog.starreveld.com/2009/11/using-symfony-di-container-with.html. But I changed ContainerFactory, because of using Symfony2 component, instead of first version.
You could also just use the PHP-DI ZF bridge: http://php-di.org/doc/frameworks/zf1.html
I know this question is really old but it pops up rather high in search engines when looking for DI in ZF1 so I thought I'd add a solution that doesn't require you to write it all by yourself.
With the Service Manager at Zend Framework 3.
Official Documentation:
https://zendframework.github.io/zend-servicemanager/
Dependencies at your Controller are usually be injected by DI Constructor injector.
I could provide one example, that inject a Factory responsible to create the ViewModel instance into the controller.
Example:
Controller
`
class JsonController extends AbstractActionController
{
private $_jsonFactory;
private $_smsRepository;
public function __construct(JsonFactory $jsonFactory, SmsRepository $smsRepository)
{
$this->_jsonFactory = $jsonFactory;
$this->_smsRepository = $smsRepository;
}
...
}
Creates the Controller
class JsonControllerFactory implements FactoryInterface
{
/**
* #param ContainerInterface $serviceManager
* #param string $requestedName
* #param array|null $options
* #return JsonController
*/
public function __invoke(ContainerInterface $serviceManager, $requestedName, array $options = null)
{
//improve using get method and callable
$jsonModelFactory = new JsonFactory();
$smsRepositoryClass = $serviceManager->get(SmsRepository::class);
return new JsonController($jsonModelFactory, $smsRepositoryClass);
}
}
`
Complete example at https://github.com/fmacias/SMSDispatcher
I hope it helps someone
Related
I started to wonder about what exactly is the purpose of service providers in Laravel, and why they work in the way they do. After searching through some articles,
the key points of service providers in my understanding are:
Simplifies object creation (Laravel What is the use of service providers for laravel)
Decoupling your code (r/laravel: When to use service providers?)
Dependency injection
Reduces technical debt
So it basically binds an implementation to an interface, and we can use it by
$app(MyInterface::class)
or something like that, and we can just change the implementation when needed, only in one place, and the rest of our code which depends on it won't break.
But i still can not grasp the concept, why they are the way they are, it seems overcomplicated. I peaked in to the code, it was certainly a ton of work to make Service Providers & Containers work, so there must be a good reason.
So to learn further, i tried to make my own, more simple version of it, which achieves the same goals. (i obviously lack a lot of info on this, and most probably missed some other goals)
My question is, why would this implementation would not satisfy the same use cases?
Service.php
namespace MyVendor;
/**
* Abstract class for creating services
*/
abstract class Service
{
/**
* Holds the instance of the provided service
*
* #var mixed
*/
private static mixed $instance = null;
/**
* Retrieves the instance of the provided service & creates it on-demand
*
* #return mixed
*/
public static function get(): mixed
{
if (self::$instance === null) {
self::$instance = static::instantiate();
}
return self::$instance;
}
/**
* A function which contains the service's object creation logic
*
* #return mixed
*/
abstract protected static function instantiate(): mixed;
}
Example implementation:
For the example, i chose an interface to parse environment variables, as i already had phpdotenv in my project as a dependency
Services/DotenvParser/DotenvParserInterface.php
namespace MyVendor\Services\DotenvParser;
/**
* This is the service interface i want to provide
*/
interface DotenvParserInterface
{
public function parse(string $directory, string $fileName = ".env"): array;
}
Now i will have 2 implementations of this class. I will pretend that a lot of my code already depends on DotenvParserInterface. An old, hacky one which "depends" on another thing, and the replacement for it which uses phpdotenv
A quick fake dependency:
Services/DotenvParser/Dependency.php
namespace MyVendor\Services\DotenvParser;
class Dependency
{
private bool $squeeze;
public string $bar;
public function __construct(string $foo, bool $squeeze)
{
$this->squeeze = $squeeze;
$this->bar = $foo;
if($this->squeeze){
$this->bar .= " JUICE";
}
}
}
Our old code:
Services/DotenvParser/OldDotenvCode.php
namespace MyVendor\Services\DotenvParser;
use BadMethodCallException;
use InvalidArgumentException;
class OldDotenvCode implements DotenvParserInterface
{
/**
* Our fake dependency
*
* #var Dependency
*/
private Dependency $foo;
private string $dir;
private string $fileName;
private string $contents;
private array $result;
public function __construct(Dependency $myDependency)
{
$this->foo = $myDependency;
}
/**
* Implementation of DotenvParserInterface
*
* #param string $directory
* #param string $fileName
* #return array
*/
public function parse(string $directory, string $fileName = ".env"): array
{
try{
$this->setDir($directory)->setFileName($fileName);
}catch(BadMethodCallException $e){
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
$this->getEnvContents();
$this->contents = $this->getEnvContents();
$this->result = [];
foreach(explode("\n", $this->contents) as $line){
$exploded = explode("=", $line);
$key = $exploded[0];
$value = (isset($exploded[1])) ? trim($exploded[1], "\r") : "";
if($this->foo->bar === "ORANGE JUICE"){
$value = trim($value, "\"");
}
$this->result[$key] = $value;
}
return $this->result;
}
#region Old, bad stuff
public function setDir(string $directory): self{
if(!\is_dir($directory)){
throw new InvalidArgumentException("Directory $directory is not a valid directory");
}
$this->dir = rtrim($directory, "/");
return $this;
}
public function setFileName(string $fileName): self{
if(empty($this->dir)){
throw new BadMethodCallException("Must call method setDir() first with a valid directory path");
}
$fileName = ltrim($fileName, "/");
if(!\file_exists($this->dir . "/" . $fileName)){
throw new InvalidArgumentException("File $fileName does not exist in provided directory {$this->dir}");
}
$this->fileName = $fileName;
return $this;
}
private function getFilePath(): string{
if(empty($this->fileName)){
throw new BadMethodCallException("Must call method setFileName() first");
}
return $this->dir . "/" . $this->fileName;
}
private function getEnvContents(): string{
return \file_get_contents($this->getFilePath());
}
public function setup(): void
{
$this->setDir($directory)->setFileName($fileName);
}
#endregion
}
Now, the phpdotenv version
Services/DotenvParser/phpdotenv.php
namespace MyVendor\Services\DotenvParser;
use Dotenv\Dotenv;
use InvalidArgumentException;
use Dotenv\Dotenv;
use InvalidArgumentException;
class phpdotenv implements DotenvParserInterface
{
public function parse(string $directory, string $fileName = ".env"): array
{
try{
Dotenv::createMutable($directory, $fileName)->load();
}catch(\Dotenv\Exception\InvalidPathException $e){
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
$result = $_ENV;
$_ENV = []; //Hehe
return $result;
}
}
Our service which we made from extending our Service class
Services/DotenvParser/DotenvParserService.php
namespace MyVendor\Services\DotenvParser;
use MyVendor\Service;
class DotenvParserService extends Service
{
// We can do this to make type hinting for ourselves
public static function get(): DotenvParserInterface
{
return parent::get();
}
protected static function instantiate(): DotenvParserInterface
{
$year = 2022;
// Some condition, to return one or another
if($year < 2022){
$dep = new \MyVendor\Services\DotenvParser\Dependency("ORANGE", true);
return new OldDotenvCode($dep);
}
return new phpdotenv();
}
}
And now, we can use it like this:
$dotenvparser = \MyVendor\Services\DotenvParser\DotenvParserService::get();
$result = $dotenvparser->parse(__DIR__);
var_dump($result);
// Outputs an array of our environment variables, yey!
We can also write tests for our services to see if anything breaks:
namespace MyVendorTest\Services\DotenvParser;
use InvalidArgumentException;
use MyVendor\Services\DotenvParser\DotenvParserInterface;
use MyVendor\Services\DotenvParser\DotenvParserService;
final class DotenvParserServiceTest extends \PHPUnit\Framework\TestCase
{
public function doesInstantiate(): void
{
$testParser = DotenvParserService::get();
$this->assertInstanceOf(DotenvParserInterface::class, $testParser);
}
public function testWorksFromValidDirNFile(): void
{
// The actual contents of a .env file
$testArray = [
"DEV_MODE" => "TRUE",
"BASE_HREF" => "http://localhost:8080/"
];
$testParser = DotenvParserService::get();
// phpdotenv loads every parent .env too and i was having none of it for this quick demonstration
$result = $testParser->parse(__DIR__."/../../../", ".env");
$this->assertEquals($testArray, $result);
}
public function testSetupFromInvalidDir(): void
{
$this->expectException(InvalidArgumentException::class);
$testParser = DotenvParserService::get();
$testParser->parse("i_am_a_dir_which_does_not_exist");
}
public function testSetupFromInvalidFile(): void
{
$this->expectException(InvalidArgumentException::class);
$testParser = DotenvParserService::get();
$testParser->parse(__DIR__, ".notenv");
}
}
So this ended up quite lenghty, but after having that Service class, you basically only need: An interface, at least one implementation of that interface, and a service class which instantiates an implementation of that interface, and optionally some tests for it. And, you can even do dependency injection with it (??) (circular dependencies would get us stuck in an endless loop), like this:
protected static function instantiate(): FooInterface
{
//BarService & AcmeService are extending our Service class
return new FooInterface(BarService::get(), AcmeService::get(), "ORANGE JUICE")
}
I am ready to absorb massive amounts of information
What other things Laravel's Service providers & containers do than i am aware of?
Why and how is it better than a simpler version, like this one?
Does my version really achieve at least those 4 key points i mentioned in the start?
I have this set of entities that we call nomenclators, which basically have an id field and a text-based field. The CRUD operations for these entities are virtually the same, just that in some of them the text field is called state while in others is area... and so on.
Given that, I created this base Controller
class NomenclatorsController extends Controller
{
use ValidatorTrait;
protected function deleteENTITYAction(Request $req, $entityName)
{
$id = $req->request->get('id');
$spService = $this->get('spam_helper');
$resp = $spService->deleteEntitySpam("AplicacionBaseBundle:$entityName", $id);
if ($resp == false)
return new JsonResponse("error.$entityName.stillreferenced", Response::HTTP_FORBIDDEN);
return new JsonResponse('', Response::HTTP_ACCEPTED);
}
protected function listENTITYAction(Request $req, $entityName)
{
$size = $req->query->get('limit');
$page = $req->query->get('page');
$spService = $this->get('spam_helper');
$objectResp = $spService->allSpam("AplicacionBaseBundle:$entityName", $size, $page);
$arrayResp = $spService->spamsToArray($objectResp);
return new JsonResponse($arrayResp, Response::HTTP_ACCEPTED);
}
protected function updateENTITYAction(Request $req, $entityName)
{
$id = $req->request->get('id');
$entity = null;
if (is_numeric($id)) {
$entity = $this->getDoctrine()->getRepository("AplicacionBaseBundle:$entityName")->find($id);
} else if (!is_numeric($id) || $id == null) {
//here comes the evil
eval('$entity=new \\AplicacionBaseBundle\\Entity\\' . $entityName . '();');
$entity->setEliminado(false);
$entity->setEmpresa($this->getUser()->getEmpresa());
}
$this->populateEntity($req->request, $entity);
$errors = $this->validate($entity);
if ($errors)
return new Response(json_encode($errors), Response::HTTP_BAD_REQUEST);
$spamService = $this->get('spam_helper');
$spamService->saveEntitySpam($entity);
}
//Override in children
protected function populateEntity($req, $entity)
{
}
}
So, each time I need to write a controller for one of these nomenclators I extend this NomenclatorsController and works like a charm.
The thing is in the updateENTITYAction I use eval for dynamic instantiation as you can see, but given all I have readed about how bad is eval I am confused now, and even when there is no user interaction in my case I want to know if there is a better way of doing this than eval and if there is any noticiable performance issue when using eval like this.
By the way I am working in a web json api with symfony and extend.js, which means no view is generated in the server,my controllers match a route and receive a sort of request params and do the work.
I've done something similar in the past. Since you are extending a base class using specific classes for each entity you can instance your entity from the controller that extends NomenclatorsController.
If one of your entities is called Foo you will have a FooController that extends NomenclatorsController. Just overwrite updateENTITYAction and pass back needed variables.
An example:
<?php
use AplicacionBaseBundle\Entity\Foo as Item;
class FooController extends NomenclatorsController
{
/**
* Displays a form to edit an existing item entity.
*
* #Route("/{id}/edit")
* #Method({"GET", "POST"})
* #Template()
* #param Request $request
* #param Item $item
* #return array|bool|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function updateENTITYAction(Request $request, Item $item)
{
return parent::updateENTITYAction($request, $item);
}
}
This way you are sending directly the entity to NomenclatorController and you don't even need to know the entityName.
Humm I'll me too advise you to avoid the eval function. It's slow and a bad practice.
What you want here is the factory pattern,
You could define a service to create the entites for you
#app/config/services.yml
app.factory.nomenclators:
class: YourNamespace\To\NomenclatorsFactory
And your factory might be like this
namespace YourNamespace\To;
use YourNamespace\To\Entity as Entites;
class NomenclatorsFactory {
// Populate this array with all your Nomenclators class names with constants OR with reflection if you have many
private $allowedNomemclators = [];
/**
* #param $entityName
* #return NomenclatorsInterface|false
*/
public function getEntity($entityName)
{
if(!is_string($entityName) || !in_array($entityName, $this->allowedNomemclators)) {
// Throw exception or exit false
return false;
}
return new $entityName;
}
}
Then you have to create the NomenclatorsInterface and define in it all the common methods between all your entities. Moreover define one more method getSomeGoodName, the job of this method is to return the good property (area or state)
With this structure your controller can only instances the Nomenclators entities and don't use anymore the eval evil method haha
Moreover you don't have to worry about about the state and area property
Ask if something isn't clear :D
I hope it help !
Looking for "AutoMapper"-like feature/framework/pattern for taking data from a Doctrine 2 domain entity / DTO and mapping the protected properties on that entity to matching public properties on a View Model.
$userEntity = $this-em->find(User::class, 1);
$userViewModel = AutoMapper->Map($userEntity, new UserViewModel());
Where the only significant difference between User and UserViewModel is that User contains get/set accessors with protected backing fields (per doctrine's instructions), whereas UserViewModel contains public properties that match in name [a subset of] the protected backing fields on User.
An thoughts on how to accomplish this? (preferably without reflection)
Note that the domain entity has public get accessor, so the solution can leverage those accessors.
Came up with my own crude, yet effective, bare-bones implementation of AutoMapper for PHP to solve this problem for me. This method will map from public properties or public getters (convention based naming) to public properties on the target entity.
Hope this helps someone out:
class Mapper
{
/**
* This method will attempt to source all public property values on $target from $source.
*
* By convention, it'll look for properties on source with the same name,
* .. and will fallback camel-cased get/set accessors to use.
*
* Note that underscores in properties will be translated to capital letters in camel-cased getters.
*
* #param $source object
* #param $target object
* #return object
* #throws Exception
*/
public static function Map($source, $target)
{
$targetProperties = get_object_vars($target);
$sourceProperties = get_object_vars($source);
foreach ($targetProperties as $name => $value)
{
//
// match properties
//
$matchingSourcePropertyExists = array_key_exists($name, $sourceProperties);
if ($matchingSourcePropertyExists)
{
$target->{$name} = $source->{$name};
continue;
}
//
// fall back on matching by convention-based get accessors
//
$sourceMethods = get_class_methods(get_class($source));
$getterName = "get" . self::convertToPascalCase($name);
$matchingGetAccessorExists = in_array($getterName, $sourceMethods);
if ($matchingGetAccessorExists)
{
$target->{$name} = $source->{$getterName}();
continue;
}
//
// if we ever fail to map an entity on the target, throw
//
$className = get_class($target);
throw new Exception("Could not auto-map property $name on $className.");
}
return $target;
}
/**
* Converts this_kind_of_string into ThisKindOfString.
* #param $value string
* #return string
*/
private static function convertToPascalCase($value)
{
$value[0] = strtoupper($value[0]);
$func = create_function('$c', 'return strtoupper($c[1]);');
return preg_replace_callback('/_([a-z])/', $func, $value);
}
}
My implementation of automapping using Symfony's components
<?php
declare(strict_types=1);
namespace App\ApiResource\Utils;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
class GenericMapper
{
public static function map(object $source, object $target): void
{
$reflectionExtractor = new ReflectionExtractor();
$sourceProperties = $reflectionExtractor->getProperties($source::class);
$propertyAccessor = PropertyAccess::createPropertyAccessor();
foreach ($sourceProperties as $propertyName) {
if ($propertyAccessor->isWritable($target, $propertyName) && $propertyAccessor->isReadable($source, $propertyName)) {
$propertyAccessor->setValue($target, $propertyName, $propertyAccessor->getValue($source, $propertyName));
}
}
}
}
I might have implementing this wrong as I cannot figure out a solid way of setting which strategy to use in my implementation of the Strategy Pattern. I'm not a big fan of writing it "statically", perhaps there is another way.
Backstory: I've done two (2) implementations (soap + http) to shipping providers in order to retrieve Track & Trace information for whatever the user inputs frontend. They each follow an interface so that I know which functions are and should (PHP :3) be available. I've shortened the class names below as this is Magento and class names are very long.
Flow: Customer inputs tracking number in a form and submits. Request is sent to the controller, controller initializes an instance of Service class, sets output via. $service->setOutput('tracking/service_gls') - note that tracking/service_gls just maps directly to the service class (Magento thing), $service->getDeliveryInformation($number) is called (we know this exists because of the interface), the entire $service object is returned to the view and data is presented.
My challenge: I'm using a switch case to set tracking/service_gls and tracking/service_otherservice then calling getDeliveryInformation(). Is this the correct approach? I feel it's a bit too static and hard to maintain if someone wants to connect another shipping provider. They would have to enter the controller and manually add another entry to the switch case, in a function somewhere 200 lines deep in the class.
Example of how the controller looks:
public function getDeliveryInformationAction()
{
$id = $this->getRequest()->getParam('id', false);
if ($id && $this->getRequest()->isAjax())
{
// SO note: service parameter is just two radio buttons with values "gls", "otherservice"
$serviceType = $this->getRequest()->getParam('service', false);
try
{
// SO note: same as doing new Class()
$service = Mage::getModel('tracking/service');
switch ($serviceType)
{
case 'gls':
$service->setOutput('tracking/service_gls');
break;
case 'other':
$service->setOutput('tracking/service_other');
break;
}
$shipment = $service->getDeliveryInformation($id);
$output = // .. create block that contains the view, $output will contain the shipment data; this is returned to the ajax request.
}
catch (Exception_RequestError $e)
{
..
}
// finally
$this->getResponse()->setHeader('content-type', 'text/html', true);
$this->getResponse()->setBody($output);
}
}
Code has been shortened a bit as there are lots more functions, but not important.
Interface the two shipping provider models are implementing
interface Output
{
/* Requests delivery information for the specified tracking number */
public function getDeliveryInformation($number);
/**
* Returns acceptor name
* #return string
*/
public function getAcceptorName();
}
Service class handles requesting data from the shipping models
class Service
{
protected $output;
/**
* Sets the output model to use
* #param string $outputType
*/
public function setOutput($outputModel)
{
// SO note: same as doing new Class()
// Magento people note: getModel() works fine tho.. ;-)
$modelInstance = Mage::app()->getConfig()->getModelInstance($outputModel);
$this->output = $modelInstance;
}
/**
* Returns delivery information for the specified tracking number
* #param string $number
* #return instance of output class
*/
public function getDeliveryInformation($number)
{
// SO note: This makes the shipping class request
// information and set data internally on the object
$this->output->getDeliveryInformation($number);
return $this->output;
}
}
Example of a shipping class; I have two in this case
class Service_Gls implements Output
{
const SERVICE_NAME = 'GLS';
const SERVICE_URL = 'http://www.gls-group.eu/276-I-PORTAL-WEBSERVICE/services/Tracking/wsdl/Tracking.wsdl';
protected $locale = 'da_DK';
/* Class constructor */
public function __construct() { }
/**
* Requests delivery information for the specified tracking number
* #param mixed $number
*/
public function getDeliveryInformation($number)
{
$this->_getDeliveryInformation($number);
}
/**
* Requests and sets information for the specified tracking number
* #param mixed $number
*/
private function _getDeliveryInformation($number)
{
// SO note: Extending from Varien_Object has magic __get, __set .. hence why there is no getData() function in this class.
if (!count($this->getData()))
{
$client = new SoapClient($url);
$client->GetTuDetail($reference));
.. set data
}
}
/**
* Returns acceptor name
* #return string
*/
public function getAcceptorName()
{
$signature = $this->getSignature();
return (isset($signature)) ? $this->getSignature() : false;
}
/**
* Returns the name of the current service
* #return string
*/
public function __toString()
{
return self::SERVICE_NAME;
}
}
Controller
class AjaxController extends Mage_Core_Controller_Front_Action
{
public function getDeliveryInformationAction()
{
$id = $this->getRequest()->getParam('id', false);
if ($id && $this->getRequest()->isAjax())
{
// SO note: service parameter is just two radio buttons with values "gls", "otherservice"
$serviceType = $this->getRequest()->getParam('service', false);
try
{
$service = Mage::getModel('tracking/service');
switch ($serviceType)
{
case 'gls':
$service->setOutput('tracking/service_gls');
break;
case 'other':
$service->setOutput('tracking/service_other');
break;
}
$shipment = $service->getDeliveryInformation($id);
$output = // .. create block that contains the view, $output will contain the shipment data; this is returned to the ajax request.
}
catch (Exception_RequestError $e)
{
..
}
// finally
$this->getResponse()->setHeader('content-type', 'text/html', true);
$this->getResponse()->setBody($output);
}
}
}
Well you either do it with a switch or with some sort of string concatenation to return the strategy class you need.
With the Strategy Pattern, choosing the correct strategy at run time is usually done through a StrategyContext pattern: https://sourcemaking.com/design_patterns/strategy/php . This allows you to isolate the algorithm to choose the correct strategy so it is not "in a function somewhere 200 lines deep in the class." .
As to the algorithm for setting the runtime strategy, personally I am a fan of class constants rather than string manipulation etc. Since the aim of the game is to arrive at a class name to instantiate, why not just a class constant to return the class name.
class OutputStrategyContext{
const SERVICE = 'tracking/service_gls';
const OTHER = 'tracking/service_other';
private $strategy;
public function __construct($serviceType)
{
$strategy = constant('self::' . strtoupper($serviceType));
$modelInstance = Mage::app()->getConfig()->getModelInstance($strategy);
$this->strategy = $modelInstance;
}
public function getStrategy()
{
return $this->strategy;
}
}
Lightweight and easy to maintain, the list of strategy classes is in one place.
You can of course make the whole thing static, or use another design pattern like an abstract factory method to acheive the same thing. Up to you really.
Anyway in the controller it is a one-liner
class AjaxController extends Mage_Core_Controller_Front_Action
{
public function getDeliveryInformationAction()
{
$id = $this->getRequest()->getParam('id', false);
if ($id && $this->getRequest()->isAjax())
{
// SO note: service parameter is just two radio buttons with values "gls", "otherservice"
$serviceType = $this->getRequest()->getParam('service', false);
try
{
$service = Mage::getModel('tracking/service');
$outputModel = new OutputStrategyContext($serviceType)->getStrategy();
$service->setOutput($outputModel);
$shipment = $service->getDeliveryInformation($id);
$output = // .. create block that contains the view, $output will contain the shipment data; this is returned to the ajax request.
}
catch (Exception_RequestError $e)
{
..
}
// finally
$this->getResponse()->setHeader('content-type', 'text/html', true);
$this->getResponse()->setBody($output);
}
}
}
Of course you have to modify the service . I also modified my context class for your code.
class Service
{
protected $output;
/**
* Sets the output model to use
* #param string $outputType
*/
public function setOutput($outputModel)
{
// SO note: same as doing new Class()
// Magento people note: getModel() works fine tho.. ;-)
$this->output = $outputModel;
}
/**
* Returns delivery information for the specified tracking number
* #param string $number
* #return instance of output class
*/
public function getDeliveryInformation($number)
{
// SO note: This makes the shipping class request
// information and set data internally on the object
$this->output->getDeliveryInformation($number);
return $this->output;
}
}
I am currently working on a project involving Zend Framework 2 and Doctrine.
I am implementing a Zend\Form and using the DoctrineORMModule\Stdlib\Hydrator\DoctrineEntity to extract and hydrate data from/to the database.
In much of the tutorials I have read, it is ideal to implement the Zend\Stdlib\Hydrator\Strategy\StrategyInterface when you need to convert a specific value before hydrating an object (in my example a date time value string which is an issue when using Doctrine). However, despite my best efforts to implement this, it seems only that the extract() method in my hydration strategy is called, never the hydrate() method. EVER!
To provide a code example, this is what I am doing - I have shortened some aspects of the code for brevity;
// Service
public function getProposerForm() {
// get required classes from service manager
$proposerEntity = $this->getServiceManager()->get('tourers_entity_proposer');
$entityManager = $this->getServiceManager()->get('Doctrine\ORM\EntityManager');
$formManager = $this->getServiceManager()->get('FormElementManager');
$proposerFieldset = $formManager->get('tourers_form_proposer_fieldset');
$proposerForm = $formManager->get('tourers_form_proposal');
$proposerFieldset->setUseAsBaseFieldset(true);
$proposerForm->add($proposerFieldset);
$proposerForm->get('submit')->setValue('Continue');
$proposerForm->bind($proposerEntity);
return $proposerForm;
}
.
// Controller
public function proposerAction() {
// grab the form from the form service
$formService = $this->getServiceLocator()->get('tourers_service_forms');
$form = $formService->getProposerForm();
if (true === $this->getRequest()->isPost()) {
$form->setData($this->getRequest()->getPost());
if (true === $form->isValid()) {
$proposerEntity = $form->getData();
$encryptedPolicyId = $formService->saveProposerForm($proposerEntity, $policyId);
return $this->redirect()->toRoute('tourers/proposal/caravan',array('policyid' => $encryptedPolicyId));
} else {
$errors = $form->getMessages();
var_dump($errors);
}
}
// view
return new ViewModel(array(
'form' => $form
,'policyid' => $policyId
)
);
}
.
// Form
class ProposerFieldset extends Fieldset implements InputFilterProviderInterface, ObjectManagerAwareInterface
{
/**
* #var Doctrine\ORM\EntityManager
*/
private $objectManager;
/**
* #return Zend\Form\Fieldset
*/
public function init()
{
// set name
parent::__construct('Proposer');
// set the hydrator to the domain object
$hydrator = new DoctrineEntity($this->objectManager,true);
$hydrator->addStrategy('proposerDateOfBirth',new DateStrategy);
$this->setHydrator($hydrator);
// other form elements below here including proposerDateOfBirth
$minDate = date('dd\/mm\/yyyy',strtotime('-100 years'));
$maxDate = date('dd\/mm\/yyyy',strtotime('-16 years'));
$this->add(array(
'name' => 'proposerDateOfBirth'
,'type' => 'Zend\Form\Element\Date'
,'attributes' => array(
'class' => 'form-control'
,'id' => 'proposerDateOfBirth'
,'placeholder' => 'dd/mm/yyyy'
,'min' => $minDate
,'max' => $maxDate
,'data-date-format' => 'dd/mm/yyyy'
),
'options' => array(
'label' => 'Date of Birth',
)
));
}
}
.
// Hydrator Strategy
namespace Tourers\Hydrator\Strategy;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class DateStrategy implements StrategyInterface {
/**
* (non-PHPdoc)
* #see Zend\Stdlib\Hydrator\Strategy.StrategyInterface::extract()
*/
public function extract($value) {
var_dump($value . ' extracted'); // GETS CALLED
return $value;
}
/**
* (non-PHPdoc)
* #see Zend\Stdlib\Hydrator\Strategy.StrategyInterface::hydrate()
*/
public function hydrate($value) {
var_dump($value . ' hydrated'); // NEVER CALLED
return $value;
}
}
I also discovered this behaviour. As yet I am still lost as to why my custom strategy was not being called.
The Strategies are applied inside the hydrateValue() and extractValue() functions, so its necessary for these functions to be called by the hydrate() and extract() functions in order for Strategies or Custom Strategies to be applied.
My problem was evident in the hydrateByReference() function inside DoctrineModule\Stdlib\Hydrator\DoctrineObject.
It seems that $this->hydrateValue(...) is only called the field has "associations". I do not yet know what these "associations" are, or what I am doing wrong for them to not exist in my data.
When I compared it to the extractByReference() function, I noticed that it always calls $this->extractValue() and does not require any "associations".
In my application, I had already implemented a custom Hydrator class. This means that when I create a form, the Hydrator and Strategies are automatically applied.
Inside my Form's init() function, I assign the custom Hydrator.
Inside my Custom Hydrator's __construct(), I add the strategies, and Custom Strategies.
So all I needed to do was to override hydrateByReference(...) in my Custom Hydrator to solve the problem. The example is below.
NOTES:
You may also need to override hydrateByValue(...) if you use "by value" hydration.
My solution may break the functionality of "associations".
My Custom Hydrator class:
class maintenance
extends DoctrineHydrator
{
/**
* Constructor
*
* #param ObjectManager $objectManager The ObjectManager to use
*/
public function __construct($objectManager)
{
/*
* Just call the parent class.
*/
parent::__construct($objectManager,
'Maintenance\Entity\maintenance', // The FQCN of the hydrated/extracted object
false // If set to true, hydrator will always use entity's public API
);
/*
* Now set up our strategies, and attach them
* to the appropriate fields.
*/
$this->addStrategy('purchasedate', new TIGDateStrategy());
$this->addStrategy('maintexpirydate', new TIGDateStrategy());
}
/**
* SF Modification, to ensure we call this->hydrateValue on
* all values before doing anything else with the data.
* This way, we convert the data first, before trying to
* store it in a DoctrineEntity.
*
* Hydrate the object using a by-reference logic (this means that values are modified directly without
* using the public API, in this case setters, and hence override any logic that could be done in those
* setters)
*
* #param array $data
* #param object $object
* #return object
*/
protected function hydrateByReference(array $data, $object)
{
$object = $this->tryConvertArrayToObject($data, $object);
$metadata = $this->metadata;
$refl = $metadata->getReflectionClass();
foreach ($data as $field => $value) {
// Ignore unknown fields
if (!$refl->hasProperty($field)) {
continue;
}
// SF Mod
$value = $this->hydrateValue($field, $value);
// End SF Mod
$value = $this->handleTypeConversions($value, $metadata->getTypeOfField($field));
$reflProperty = $refl->getProperty($field);
$reflProperty->setAccessible(true);
if ($metadata->hasAssociation($field)) {
$target = $metadata->getAssociationTargetClass($field);
if ($metadata->isSingleValuedAssociation($field)) {
$value = $this->toOne($target, $this->hydrateValue($field, $value));
$reflProperty->setValue($object, $value);
} elseif ($metadata->isCollectionValuedAssociation($field)) {
$this->toMany($object, $field, $target, $value);
}
} else {
$reflProperty->setValue($object, $value);
}
}
return $object;
}
}