Callback on serializer Symfony - php

I'm running Symfony 2.7 and I'm trying output an object (Doctrine entity) as JSON.
When I'm normalizing the object I want to convert some of it's values. To do this I found the "setCallbacks" method in the documentation but I'm kinda stumped on how to apply it to my case.
Is there any way to call the "setCallbacks" method on the normalizer that is set when calling Symfonys serializer service?
Here is a short example of what I'm trying to achieve:
//ExampleController.php
public function getJSONOrderByIdAction($id) {
$serializer = $this->get('serializer');
$normalizer = $serializer->getNormalizer(); // <- This is what I'm unable to do
$dateTimeToString = function ($dateTime) {
return $dateTime instanceof \DateTime ? $dateTime->format(\DateTime::ISO8601) : '';
};
$normalizer->setCallbacks(['time' => $dateTimeToString]);
$order = $this->getDoctrine()->find("AppBundle:Order", $id);
return new JsonResponse(["order" => $serializer->normalize($order, null, ["groups" => ["public"]])]);
}
I'm aware that most people have switched to the JMS serializer. It just seems as if the built in serializer should be able to handle what I'm trying to achieve.

The default Serializer service is created during dependency injection phase, and the Serializer interface do not allow editing of (full) retrieval of normalizers.
I think you have (at least) three choice here:
add your custom normalizer to the default Serializer service
add NormalizableInterface to your entities
create a new Serializer service (or a local object as suggested by the docs) as you were trying to do.
I think in your scenario, case 1 is preferred (since 2 becomes boring pretty fast).
I would do something like this; first create a custom Normalizer
<?php
namespace AppBundle;
class DateTimeNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
{
/**
* {#inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return $object->format(\DateTime::ISO8601);
}
/**
* {#inheritdoc}
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
return new $class($data);
}
/**
* Checks if the given class is a DateTime.
*
* #param mixed $data Data to normalize.
* #param string $format The format being (de-)serialized from or into.
*
* #return bool
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof \DateTime;
}
/**
* Checks if the given class is a DateTime.
*
* #param mixed $data Data to denormalize from.
* #param string $type The class to which the data should be denormalized.
* #param string $format The format being deserialized from.
*
* #return bool
*/
public function supportsDenormalization($data, $type, $format = null)
{
$class = new \ReflectionClass($type);
return $class->isSubclassOf('\DateTime');
}
}
Then register it to your services:
# app/config/services.yml
services:
datetime_normalizer:
class: AppBundle\DateTimeNormalizer
tags:
- { name: serializer.normalizer }

My own solution
Following the advice from giosh94mhz I tried switching to JMS Serializer but ended up going back to Symfonys serializer.
JMS Serializer presented it's own issues and while searching for answers for those I stumbled upon a blog post by Thomas Jarrand that did an excellent job explaining how to make and implement your own normalizers in Symfony.

You can use callback normalizer of K. Dunglas component.
You can see that in ObjectNormalizer (in normalize method)
if (isset($this->callbacks[$attribute])) {
$attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue);
}
This mean that you must use in callback array key the name of property you wan't to normalize.
For example in my entity I have field named "name" of type "pgarray" (like array for postgresql). I wan't to normalize this data. Instead array I want a string.
/**
* $object represent the property "name" because callback is attached to name property (setCallback)
*/
$nameCallback = function ($object, $outerObject = null) {
return $object[0];
};
$this->normalizer->setCallbacks(['name' => $dateCallback]);
Just remember since Symfony 4.2 you must use $context in DI to use callback.

In my opinion, you seem to be trying to over-complicate things. Here's the approach I've taken when I needed to serialize my entities as JSON:
PHP 2.5 and above allows you to implement the jsonSerialize method on your objects and just call json_encode directly on your object.
If you are still using PHP 2.4, you just need to manually call jsonSerialize() on your objects.
For example:
/**
* #ORM\Entity
*/
class MyEntity {
...
public function jsonSerialize() {
$data = array("foo" => $this->bar());
// add other data here ...
return $data
}
}
And then in calling code:
// for PHP 2.5 and up:
$normalized = json_encode($myEntityInstance);
// for PHP 2.4 and below
$normalized = json_encode($myEntityInstance->jsonSerialize());

Related

symfony dynamic entity instantiation with php eval

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 !

Validating objects with Zend\Validator

I'm trying to use Zend\Validator to validate objects but I find it hard for a couple of reasons and am now wondering if I'm doing something fundamentally wrong or if the component is just not a good choice to do so…
Ideally I'd like to run
$objectValidator = new ObjectValidator();
$objectValidator->isValid($object);
So in this case I would put (sub-)validators for the objects properties in ObjectValidator's isValid() method for example like this:
public function isValid($value, $context = null)
{
$propertyValidator = new Zend\Validator\Callback(function($value) {
return false;
});
if (!$propertyValidator->isValid($value)) {
foreach ($propertyValidator->getMessages() as $code => $message) {
$this->abstractOptions['messages'][$code] = $message;
}
return false;
}
return true;
}
The way to merge the messages from the property's validator I've copied from the component's EmailAddress validator that falls back on the Hostname validator.
The trouble starts when I'm using a type of validator twice (e.g. Callback) no matter if on the same property or a different since the messages are merged and I'm loosing information that I'd like to have. I could build a way to manage the messages myself but I'm wondering if there's not a better solution.
I also thought of using Zend\InputFilter instead creating Zend\Inputs for each property that I want to run checks on. This way I certainly get all the messages but it adds a rather annoying task of dismantling the object before I can validate it.
Any input highly appreciated.
I would suggest to use the ZF2 InputFilter class as a base for deep validating objects and properties.
You can implement Zend\Stdlib\ArraySerializableInterface interface to solve the "annoying task of dismantling the object" issue with a getArrayCopy method:
<?php
namespace Application\Model;
use Zend\Stdlib\ArraySerializableInterface;
class MyObject implements ArraySerializableInterface
{
/**
* Exchange internal values from provided array
*
* #param array $array
* #return void
*/
public function exchangeArray(array $array)
{
//...Your custom logic to exchange properties with data from an array
}
/**
* Return an array representation of the object
*
* #return array
*/
public function getArrayCopy()
{
//...Your custom logic to get array copy of the object for validation
}
}
Or make a custom hydrator class that does this for you...

Yii 1.1 General purpose web service for crud operations

Using Yii 1.1, I consider creating a soap based web service which performs common CRUD operations on models. For example I just want to make a single CreateObject function for all models which receives some parameters, finds the model on the fly and assigns those parameters to appropriate model attributes.
for example for User model I've created the following action in my web service controller:
/** Create a new User record
* #param string username
* #param string password
* #param string family
* #param string name
* #param string title
* #param string email
* #param string mobile
* #return mixed true on success or an array of validation errors on failure
* #soap
*/
public function createUser($username, $password, $family, $name="", $title="", $email="",$mobile="")
{
$newUser = new User();
$newUser->username = $username;
$newUser->password = $password;
$newUser->family = $family;
$newUser->name = $name;
$newUser->title = $title;
$newUser->email = $email;
$newUser->mobile = $mobile;
//Get user roles
/* if (is_array($roles) && !empty($roles)) {
$roleList = User::getRoleList();
$newUser->_roles = array_intersect($roles, $roleList);
} else $newUser->_roles = array();*/
if($newUser->save())
return true;
else
return $newUser->getErrors();
}
This function is called via a web service to create a new user and it works fine. However I have to do the same thing to create a new object from the rest of models.
I have heard about entity pattern or entity factory which may help me to archive this goal but I've not found any good tutorial about that.
Any suggestion is highly appreciated.
Using interfaces or inheritance with SOAP is quite tricky, so an easier aproach would be to modify your SOAP web service so that it will receive two strings, one for the name of the model and the other a json string with the attributes for your model, so you can instanciate your model in the following way:
public function create($model,$attributes){
$model=new $modelName;
}
Also you can use the php json functions to decode the attributes encoded as a json object.
UPDATE
If you need some complex logic for the initialization of your models, you could try using either a factory method or a Factory class. A really simple example could be something like this:
class ModelsFactory{
//use constants to identify the classes that you can return
const Model1=1;
const Model2=2;
public static getInstance($model){
$modelInstance=null;
switch($model){
case Model1:
//... initialize a model1 class and asign to $modelInstance
case Model2:
//...
}
return $modelInstance;
}
}
As you can see you could initialize complex classes in an easy way. Also you can extend it to your needs. Also if you need parameters just add them to the creator method in the form of an object or as an associative array.
Hope this helps!

zend framework 2 doctrine hydration strategy - hydrate method never called

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;
}
}

How to use dependency injection in Zend Framework?

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

Categories