How to use \Zend\Cache andZend\Db\RowGateway\RowGateway together? - zend-framework2

I have an abstract table class with code similar to this:
function fetchById($id) {
$id = (int) $id;
$cacheName = sprintf('%s-%s', stripslashes(get_class($this)), hash('sha256', sprintf(
'%s-%s-%s', get_class($this), __FUNCTION__, $id
)));
if (($row = $this->cache->getItem($cacheName)) == FALSE) {
$rowset = $this->tableGateway->select(array('id' => $id));
$row = $rowset->current();
if (!$row) {
throw new \Exception("Could not find row $id");
}
$this->cache->setItem($cacheName, $row);
}
return $row;
}
This works well enough for the default ArrayObject that $row is returned as, however I am now wanting to include additional functionality into my row objects (so that the functionality is not included in multiple, unrelated, places such as different controllers, etc).
To this end I have created an ArrayObjectPrototype extending Zend\Db\RowGateway\RowGateway, however when I try to cache the row I am getting the following error message: You cannot serialize or unserialize PDO instances
Oh dear.
I have no problem adding __wake and __sleep functions to my row object, but how do I get the PDO object into my __wake function?
I am creating my cache in my application.config.php file:
'ZendCacheStorageFactory' => function() {
return \Zend\Cache\StorageFactory::factory(
array(
'adapter' => array(
'name' => 'filesystem',
'options' => array(
'dirLevel' => 2,
'cacheDir' => 'data/cache',
'dirPermission' => 0755,
'filePermission' => 0666,
),
),
'plugins' => array('serializer'),
)
);
},
I assume I have to create a custom plugin that I pass the db adaptor into? But I am totally lost on how to do that.

What I have done (perhaps incorrectly) is create an abstract row class that does not save the pdo instance. This class extends the RowGateway class but overrides the __construct, initialize, save and delete functions (all the functions that require the pdo
/**
* Abstract table row class.
*
* #package RPK
* #subpackage Db
* #author SuttonSilver
*/
namespace RPK\Db;
use Zend\Db\Adapter\Adapter;
use Zend\Db\RowGateway\Exception\RuntimeException;
use Zend\Db\RowGateway\RowGateway;
use Zend\Db\Sql\Sql;
use Zend\Db\Sql\TableIdentifier;
use Zend\Db\RowGateway\Feature\FeatureSet;
abstract class RowAbstract extends RowGateway {
/**
* Constructor
*
* #param string $primaryKeyColumn
* #param string|TableIdentifier $table
* #param Adapter|Sql $adapterOrSql
* #throws Exception\InvalidArgumentException
*/
public function __construct($primaryKeyColumn, $table, $adapterOrSql = null) {
// setup primary key
$this->primaryKeyColumn = empty($primaryKeyColumn) ? null : (array) $primaryKeyColumn;
// set table
$this->table = $table;
$this->initialize();
}
/**
* initialize()
*/
public function initialize() {
if ($this->isInitialized) {
return;
}
if (!$this->featureSet instanceof FeatureSet) {
$this->featureSet = new FeatureSet;
}
$this->featureSet->setRowGateway($this);
$this->featureSet->apply('preInitialize', []);
if (!is_string($this->table) && !$this->table instanceof TableIdentifier) {
throw new RuntimeException('This row object does not have a valid table set.');
}
if ($this->primaryKeyColumn === null) {
throw new RuntimeException('This row object does not have a primary key column set.');
} elseif (is_string($this->primaryKeyColumn)) {
$this->primaryKeyColumn = (array) $this->primaryKeyColumn;
}
$this->featureSet->apply('postInitialize', []);
$this->isInitialized = true;
}
/**
* Save any changes to this row to the table
* #throws \Exception
*/
public function save() {
throw new \Exception('No PDO object available.');
}
/**
* Delete this row from the table
* #throws \Exception
*/
public function delete() {
throw new \Exception('No PDO object available.');
}
}

Related

ZF FactoryInterface - using options parameter for configuring loading dependencies

I am wondering about the best practices for loading complex objects.
To begin with, i'm going to outline some boilerplate before getting to the problem.
Assume the following: A simple domain model Client is loaded using a tablegateway, with factories used at every stage to inject dependencies:
namespace My\Model\Client;
class Client implements InputFilterProviderInterface
{
/**#var integer*/
protected $id;
/**#var InputFilter*/
protected $inputFilter;
/**#var Preferences */
protected $preferences;
/**#var Orders*/
protected $orders;
/**#var Contacts*/
protected $contacts;
}
A factory for this Client object:
namespace My\Model\Client;
class ClientFactory implements FactoryInterface
{
public function($container, $requestedName, $options)
{
$client = new Client();
$client->setInputFilter($container->get('InputFilterManager')->get('ClientInputFilter'));
return $client;
}
}
Next the mapper factory, which uses a TableGateway:
namespace My\Model\Client\Mapper;
class ClientMapperFactory implements FactoryInterface
{
public function __invoke($container, $requestedName, $options)
{
return new ClientMapper($container->get(ClientTableGateway::class));
}
}
The TableGatewayFactory:
namespace My\Model\Client\TableGateway
class ClientTableGatewayFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$hydrator = new ArraySerialisable();
$rowObjectPrototype = $container->get(Client::class);
$resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
$tableGateway = new TableGateway('clients', $container->get(Adapter::class), null, $resultSet);
return $tableGateway;
Note the use of a HydratingResultset to return fully formed Client objects from the ResultSet.
This all works nicely.
Now the Client object has several related objects as properties, so whilst using the HydratingResultSet, i'm going to add an AggregateHydrator to load them:
class ClientTableGatewayFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
**$hydrator = $container->get('HydratorManager')->get(ClientHydrator::class);**
$rowObjectPrototype = $container->get(Client::class);
$resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
$tableGateway = new TableGateway('clients', $container->get(Adapter::class), null, $resultSet);
return $tableGateway;
}
Finally, the Clients hydrator factory:
class ClientHydratorFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
//base ArraySerializable for Client object hydration
$arrayHydrator = new ArraySerializable();
$arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());
$aggregateHydrator = new AggregateHydrator();
$aggregateHydrator->add($arrayHydrator);
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsAddressHydrator::class));
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsOrdersHydrator::class));
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsPreferencesHydrator::class));
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsContactsHydrator::class));
return $aggregateHydrator;
}
}
... with the gist of the above hydrators being like:
class ClientsAddressHydrator implements HydratorInterface
{
/** #var AddressMapper */
protected $addressMapper;
public function __construct(AddressMapper $addressMapper){
$this->addressMapper = $addressMapper;
}
public function extract($object){return $object;}
public function hydrate(array $data, $object)
{
if(!$object instanceof Client){
return;
}
if(array_key_exists('id', $data)){
$address = $this->addressMapper->findClientAddress($data['id']);
if($address instanceof Address){
$object->setAddress($address);
}
}
return $object;
}
}
Finally we're at the issue. The above works perfectly and will load quite cleanly a Client object with all the related objects fully formed. But i have some resources where the entire object graph is not needed - for instance, when viewing a table of all clients - there is no need for any more information to be loaded.
So i've been thinking of ways of using the factories to choose which dependencies to include.
Solution 1
A factory for each use case. If only the Client data is needed (with no dependencies), then create a series of factories ie ClientFactory, SimpleClientFactory, ComplexClientFactory, ClientWithAppointmentsFactory etc. Seems redundant and not very reusable.
Solution 2
Use the options param defined in the FactoryInterface to pass "loading" options to the hydrator factory, eg:
class ViewClientDetailsControllerFactory implements FactoryInterface
{
//all Client info needed - full object graph
public function __invoke($container, $requestedName, $options)
{
$controller = new ViewClientDetailsController();
$loadDependencies = [
'loadPreferences' => true,
'loadOrders' => true,
'loadContacts' => true
];
$clientMapper = $container->get(ClientMapper::class, '', $loadDependencies);
return $controller;
}
}
class ViewAllClientsControllerFactory implements FactoryInterface
{
//Only need Client data - no related objects
public function __invoke($container, $requestedName, $options)
{
$controller = new ViewAllClientsController();
$loadDependencies = [
'loadPreferences' => false,
'loadOrders' => false,
'loadContacts' => false
];
$clientMapper = $container->get(ClientMapper::class, '', $loadDependencies);
return $controller;
}
}
The mapper factory passes the options to the tablegateway factory, that passes them on to the hydrator factory:
class ClientTableGatewayFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$hydrator = $container->get('HydratorManager')->get(ClientHydrator::class, '', $options);
$rowObjectPrototype = $container->get(Client::class);
$resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
$tableGateway = new TableGateway('clients', $container->get(Adapter::class), null, $resultSet);
return $tableGateway;
}
Finally, we can define here how much info to load into the Client:
class ClientHydratorFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
//base ArraySerializable for Client object hydration
$arrayHydrator = new ArraySerializable();
$arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());
$aggregateHydrator = new AggregateHydrator();
$aggregateHydrator->add($arrayHydrator);
if($options['loadAddress'] === true){
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsAddressHydrator::class));
}
if($options['loadOrders'] === true){
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsOrdersHydrator::class));
}
if($options['loadPreferences'] === true){
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsPreferencesHydrator::class));
}
if($options['loadContacts'] === true){
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsContactsHydrator::class));
}
return $aggregateHydrator;
}
}
This seems to be a clean solution, as the dependencies can be defined per request. However i don't think that this is using the options param as intended - the documentation states that this parameter is supposed to be for passing constructor params to the object, not defining what logic the factory should use to load dependencies.
Any advice, or alternative solutions to achieve the above, would be great. Thanks for reading.
Creating a big palette of all possible combinations would not be just a nightmare, but a declared suicide.
Using options
I wouldn't suggest you this option either. I mean, it's not that bad, but it has a major issue: everytime you instantiate your hydrator, you should remember to pass those options, or you'll get an "empty hydrator". Same logic applies to everything that uses those hydrators.
Since you actually want to remove hydrators you don't need, I'd suggest to avoid this solution, because this way you are always forced to declare which hydrators you need (and, honestly, I'll always forget to do it.. ^^ ).
If you add a new hydrator, you'll have to go through your project and add new options. Not really worth the effort...
That's why I propose you the next solution
Removing unnecessary hydrators
In 99% of the cases, hydrators are used by mappers. Thus, I think it would be cleanier to have a mapper which, by default, returns always the same kind of data (->a single hydrator), but that it can be modified to remove a certain set of hydrators.
Inside the AggregateHydrator, all hydrators are converted into listeners and attached to EventManager. I had some issue while trying to get all events, so I turned on creating an aggregate hydrator with the option to detach an hydrator:
class DetachableAggregateHydrator extends AggregateHydrator
{
/**
* List of all hydrators (as listeners)
*
* #var array
*/
private $listeners = [];
/**
* {#inherit}
*/
public function add(HydratorInterface $hydrator, int $priority = self::DEFAULT_PRIORITY): void
{
$listener = new HydratorListener($hydrator);
$listener->attach($this->getEventManager(), $priority);
$this->listeners[get_class($hydrator)] = $listener;
}
/**
* Remove a single hydrator and detach its listener
*
* #param string $hydratorClass
*/
public function detach($hydratorClass)
{
$listener = $this->listeners[$hydratorClass];
$listener->detach($this->getEventManager());
unset($listener);
unset($this->listeners[$hydratorClass]);
}
}
Then, in the TableGatewayFactory:
class ClientTableGatewayFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$hydrator = $container->get('HydratorManager')->get(ClientHydrator::class);
$rowObjectPrototype = $container->get(Client::class);
$resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
$adapter = $container->get(Adapter::class);
$tableGateway = new TableGateway('clients', $adapter, null, $resultSet);
return $tableGateway;
}
}
And the ClientHydratorFactory:
class ClientHydratorFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$aggregateHydrator = new DetachableAggregateHydrator();
$arrayHydrator = new ArraySerializable();
$arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());
$aggregateHydrator->add($arrayHydrator);
$hydratorManager = $container->get('HydratorManager');
$aggregateHydrator->add($hydratorManager->get(ClientsAddressHydrator::class));
$aggregateHydrator->add($hydratorManager->get(ClientsOrdersHydrator::class));
$aggregateHydrator->add($hydratorManager->get(ClientsPreferencesHydrator::class));
$aggregateHydrator->add($hydratorManager->get(ClientsContactsHydrator::class));
return $aggregateHydrator;
}
}
You just need to make tablegateway accessible by outstide the mapper:
class ClientMapper
{
private $tableGateway;
// ..
// Other methods
// ..
public function getTableGateway(): TableGateway
{
return $this->tableGateway;
}
}
And now you're able to choose which hydrators you don't want to attach.
Let's say you have two controllers:
ClientInfoController, where you need clients and their address, preferences and contacts
ClientOrdersController, where you need clients with their orders
Their factories will be:
class ClientInfoController implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$clientMapper = $container->get(ClientMapper::class);
// Orders are unnecessary
$resultSetPrototype = $clientMapper->getTableGateway()->getResultSetPrototype();
$resultSetPrototype->getHydrator()->detach(ClientsOrdersHydrator::class);
return $aggregateHydrator;
}
}
class ClientOrdersController implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$clientMapper = $container->get(ClientMapper::class);
// Orders are unnecessary
$resultSetPrototype = $clientMapper->getTableGateway()->getResultSetPrototype();
$resultSetPrototype->getHydrator()->detach(ClientsAddressHydrator::class);
$resultSetPrototype->getHydrator()->detach(ClientsPreferencesHydrator::class);
$resultSetPrototype->getHydrator()->detach(ClientsContactsHydrator::class);
return $aggregateHydrator;
}
}

FormElementError for login failed

I am very new to Zend Framework 2. When I try to login on false credentials it don't show any error. So what should be the code to display FormElementError in this case.
My LoginFilter.php is given below:
use Zend\InputFilter\InputFilter;
class LoginFilter extends InputFilter
{
public function __construct()
{
$this->add(array(
'name' => 'email',
'required' => true,
'validators' => array(
array(
'name' => 'EmailAddress',
'options' => array(
'domain' => true,
),
),
),
));
$this->add(array(
'name' => 'password',
'required' => true,
//'validators' => array(),
));
}
}
So this is how I do it with a custom InputFilter. I apply the setMessage to the Zend-Validator, which is being stuffed into the ValidatorChain, via setMessage().Below you can see $emailDoesNotExist->setMessage('This e-mail address is already in use'); is here I set my error message. You can also do it to like StringLength variable ($stringLength).
namespace User\InputFilter;
use Zend\Db\Adapter\Adapter;
use Zend\Filter\FilterChain;
use Zend\Filter\StringTrim;
use Zend\I18n\Validator\Alnum;
use Zend\InputFilter\Input;
use Zend\InputFilter\InputFilter;
use Zend\Validator\Db\NoRecordExists;
use Zend\Validator\EmailAddress;
use Zend\Validator\Identical;
use Zend\Validator\Regex;
use Zend\Validator\StringLength;
use Zend\Validator\ValidatorChain;
class AddUser extends InputFilter
{
/**
*
* #var InputFilter $dbAdapter
*/
protected $dbAdapter;
public function __construct(Adapter $dbAdapter)
{
$this->dbAdapter = $dbAdapter;
$firstName = new Input('first_name');
$firstName->setRequired(true);
$firstName->setValidatorChain($this->getNameValidatorChain());
$firstName->setFilterChain($this->getStringTrimFilterChain());
$lastName = new Input('last_name');
$lastName->setRequired(true);
$lastName->setValidatorChain($this->getNameValidatorChain());
$lastName->setFilterChain($this->getStringTrimFilterChain());
$email = new Input('email');
$email->setRequired(true);
$email->setValidatorChain($this->getEmailValidatorChain());
$email->setFilterChain($this->getStringTrimFilterChain());
$password = new Input('password');
$password->setRequired(true);
$password->setValidatorChain($this->getPasswordValidatorChain());
$password->setFilterChain($this->getStringTrimFilterChain());
$repeatPassword = new Input('repeat_password');
$repeatPassword->setRequired(true);
$repeatPassword->setValidatorChain($this->getRepeatPasswordValidatorChain());
$repeatPassword->setFilterChain($this->getStringTrimFilterChain());
$this->add($firstName);
$this->add($lastName);
$this->add($email);
$this->add($password);
$this->add($repeatPassword);
}
/**
* Gets the validation chain for the first name and last name inputs
*
* #return ValidatorChain
*/
protected function getNameValidatorChain()
{
$stringLength = new StringLength();
$stringLength->setMin(2);
$stringLength->setMax(50);
$validatorChain = new ValidatorChain();
$validatorChain->attach(new Alnum(true));
$validatorChain->attach($stringLength);
return $validatorChain;
}
/**
* Gets the validation chain for the email input
*
* #return ValidatorChain
*/
protected function getEmailValidatorChain()
{
$stringLength = new StringLength();
$stringLength->setMin(2);
$stringLength->setMax(50);
$emailDoesNotExist = new NoRecordExists(
array(
'table' => 'user',
'field' => 'email',
'adapter' => $this->dbAdapter
)
);
$emailDoesNotExist->setMessage('This e-mail address is already in use');
$validatorChain = new ValidatorChain();
$validatorChain->attach($stringLength, true);
$validatorChain->attach(new EmailAddress(), true);
$validatorChain->attach($emailDoesNotExist, true);
return $validatorChain;
}
/**
*
* #return ValidatorChain
*/
protected function getPasswordValidatorChain()
{
$stringLength = new StringLength();
$stringLength->setMax(6);
$oneNumber = new Regex('/\d/'); //checking to make sure it has at least one number
$oneNumber->setMessage('Must contain at least one number');
$validatorChain = new ValidatorChain();
$validatorChain->attach($stringLength);
$validatorChain->attach($oneNumber);
return $validatorChain;
}
/**
*
* #return ValidatorChain
*/
protected function getRepeatPasswordValidatorChain()
{
$identical = new Identical();
$identical->setToken('password'); // Name of the input whose value to match
$identical->setMessage('Passwords must match');
$validatorChain = new ValidatorChain();
$validatorChain->attach($identical);
return $validatorChain;
}
/**
*
* #return \User\InputFilter\FilterChain
*/
protected function getStringTrimFilterChain()
{
$filterChain = new FilterChain();
$filterChain->attach(new StringTrim());
return $filterChain;
}
}

Dynamic generated form with Zend Framework 2

I'm creating ZF2 Poll Module. I have poll with many questions. Every question has answers that can be multiple answers or single answer(Radio or MultiCheckbox). How to create a dynamic form that I can show to front-end?
This is what I've tried, but the form doesn't validate correctly...
module\Polls\src\Polls\Form\PollFillingQuestionsForm.php
<?php
namespace Polls\Form;
use Zend\Form\Form;
use Polls\Form\Fieldset\PollFillingQuestionAnswerFieldset;
use Polls\Form\Fieldset\PollFillingQuestionFieldset;
class PollFillingQuestionsForm extends Form {
public function __construct($questionsObject) {
parent::__construct('questionsForm');
$questionsFieldset = new PollFillingQuestionFieldset('questions');
//$questionsObject is array of question objects.
foreach ($questionsObject as $questionObject) {
$fieldset = new PollFillingQuestionAnswerFieldset($questionObject->id, array(), $questionObject);
$questionsFieldset->add($fieldset);
}
$this->add($questionsFieldset);
$this->add(array(
'name' => 'submit',
'attributes' => array(
'type' => 'submit',
'value' => 'Submit Poll',
'class' => 'btn btn-success',
),
));
}
}
module\Polls\src\Polls\Form\Fieldset\PollFillingQuestionAnswerFieldset.php
<?php
namespace Polls\Form\Fieldset;
use Polls\Model\QuestionAnswer;
use Zend\Form\Fieldset;
use Zend\Stdlib\Hydrator\ArraySerializable;
class PollFillingQuestionAnswerFieldset extends Fieldset {
public function __construct($name, $options, $questionObject) {
parent::__construct($name, $options);
$question = $questionObject;
$this->setLabel($question->title);
$type = 'Radio';
$elementType = 'radio';
switch ($question->answer_type) {
case 'many':
$type = 'MultiCheckbox';
$elementType = 'checkbox';
break;
case 'one':
$type = 'Radio';
$elementType = 'radio';
break;
default:
$type = 'Radio';
$elementType = 'radio';
break;
}
$this->setHydrator(new ArraySerializable())
->setObject(new QuestionAnswer());
$answers = $question->getAnswers();
$answerValues = array();
foreach ($answers as $answer) {
$answerValues[$answer->id] = $answer->title;
}
$this->add(array(
'name' => 'answer',
'type' => $type,
'options' => array(
'type' => $elementType,
'value_options' => $answerValues,
),
));
}
}
I've done this in the past, with a clean Factory strategy you can inject the dependencies into your form and your input filter. The magic lies in your Factories.
Start by wiring things in your service manager config:
'form_elements' => [
'factories' => [
DynamicForm::class => DynamicFormFactory::class,
],
],
'input_filters' => [
'factories' => [
DynamicInputFilter::class => DynamicInputFilterFactory::class,
],
],
First task is to get your FormFactory done up right.
class DynamicFormFactory implements FactoryInterface, MutableCreationOptionsInterface
{
/**
* #var array
*/
protected $options;
/**
* Set creation options
*
* #param array $options
* #return void
*/
public function setCreationOptions( array $options )
{
$this->options = $options;
}
/**
* {#inheritdoc}
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
/**
* #var \Zend\Form\FormElementManager $serviceLocator
* #var \Zend\ServiceManager\ServiceManager $serviceManager
*/
$serviceManager = $serviceLocator->getServiceLocator();
try
{
$options = /* set up your form's config, you have the service manager here */;
$form = new DynamicForm( $options );
$form->setInputFilter( $serviceManager->get('InputFilterManager')->get( DynamicFormFilter::class, $options ) );
}
catch( \Exception $x )
{
die( $x->getMessage() );
}
return $form;
}
}
Then, react to $options in your DynamicInputFilterFactory through the MutableCreationOptionsInterface implementation. You generally don't want forms and filters to be 'option aware', let the factories take care of that.
class DynamicInputFilterFactory implements FactoryInterface, MutableCreationOptionsInterface
{
protected $options;
/**
* Set creation options
*
* #param array $options
* #return void
*/
public function setCreationOptions( array $options )
{
$this->options = $options;
}
public function createService( ServiceLocatorInterface $serviceLocator )
{
/* do stuff with $this->options */
return new DynamicInputFilter(
/* pass your transformed options */
);
}
}
Next, all you have to do is create your form and input filter per what was passed to them through MutableOptions. Set your dependencies in __construct (don't forget to call parent::__construct) and initialize your form in init per the options passed in.
I suspect you have a good base in ZF2, so I'll stop here. This ought to get you on your way. Take-aways are MutableCreationOptionsInterface and separating your InputFilter and Form construction, combining the two in your Form Factory.

Zend\Form\Form not displayin error message from custom validator

I'v creatated a custom validator:
class MyValidator extends AbstractValidator
{
const ERROR_CONST = 'error';
protected $dbAdapter;
protected $messageTemplates = array(
self::ERROR_CONST => "Error msg for '%value%'."
);
public function __construct($dbAdapter)
{
$this->dbAdapter = $dbAdapter;
}
public function isValid($value, $context = null)
{
$this->setValue($value);
/**
* Do validation against db
*/
if(/* Not valid */){
$this->error(self::ERROR_CONST);
return false;
}
return true;
}
}
The validation work, I've been able to test it. What doesn't work is the output of the error message using
echo $this->formElementErrors($form->get('action'));
All that is outputted is an empty UL. Is this a translator issue? When I do a get_class on $this->getTranslator() in the validator I get the validator class name. When I var_dump $this->getTranslator() it outputs null. Do I need to set a translator for this to work and where would be the best place to set that translator so that it's system wide for my own validators?
Because you define a __construct method for your validator class, the parent __construct is not implicitly called:
http://php.net/manual/en/language.oop5.decon.php (see the note)
You should modify your __construct method:
public function __construct($dbAdapter)
{
$this->dbAdapter = $dbAdapter;
//parent::__construct($options);
parent::__construct(null); // or (void)
}
As you can see, $messageTemplates and $messageVariables are "loaded" from AbstractValidator::__construct, for being used in some methods ( error included):
https://github.com/zendframework/zf2/blob/master/library/Zend/Validator/AbstractValidator.php#L73-L79
Maybe you forgot to add messageVariables ?
/**
* Message variables
* #var array
*/
protected $messageVariables = array(
'value' => 'value',
);

How to create a service layer in zend framwork two?

I need to create a service layer for Zend framework two controller functions in order to decouple the services from controllers.
You're going to need to use the ServiceManager (SM) in order to make this work properly.
This is just an example of how I have done it:
In your ModuleName/src/ModuleName/ create a folder named Service and create your ExampleService.php, Example:
namespace ModuleName\Service;
class ExampleService
{
public function SomeFunctionNameHere()
{
echo 'Hello World';
}
}
Now edit your Module.php and add the Service Layer to your invokables, IE:
public function getServiceConfig()
{
return array(
'invokables' => array(
'ModuleName\Service\ExampleService' => 'ModuleName\Service\ExampleService',
),
);
}
Now edit your ModuleNameController.php
protected $service_example;
public function indexAction()
{
$service = $this->getServiceExample()->SomeFunctionNameHere();
}
private function getServiceExample()
{
if (!$this->service_example) {
$this->service_example = $this->getServiceLocator()->get('ModuleName\Service\ExampleService');
}
return $this->service_example;
}
This should get you started.
Depending on the functionality you are looking for from your service, you might be able to create a custom Controller Plugin. For example, here's a custom controller plugin I wrote to get a user's access level.
Application/Controller/Plugin/GetAccessLevel.php
namespace Application\Controller\Plugin;
use Zend\Mvc\Controller\Plugin\AbstractPlugin;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
class GetAccessLevel extends AbstractPlugin implements ServiceLocatorAwareInterface
{
/**
* Set the service locator.
*
* #param ServiceLocatorInterface $serviceLocator
* #return GetAccessLevel
*/
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
/**
* Get the service locator.
*
* #return \Zend\ServiceManager\ServiceLocatorInterface
*/
public function getServiceLocator()
{
return $this->serviceLocator;
}
/**
* Takes an array of role objects and returns access level
*
* #param array of MyModule\Entity\Role objects
* #return int Access Level
*/
public function __invoke(array $roles)
{
// Default access level
$accesslevel = 0;
// Get Service Locator for view helpers
$controllerPluginManager = $this->getServiceLocator();
// Get application service manager
$serviceManager = $controllerPluginManager->getServiceLocator();
// Get application config
$config = $serviceManager->get('Config');
// Get the role associated with full access from config
$fullAccessRole = $config['appSettings']['full_access_role'];
// Does user have the role for full access?
foreach ($roles as $roleObject) {
if($roleObject->getName() == $fullAccessRole) {
$accesslevel = 1;
break;
}
}
// Return access level
return $accesslevel;
}
}
Then add the plugin to the configuration.
./module/Application/config/module.config.php
'controller_plugins' => array(
'invokables' => array(
'getAccessLevel' => 'Application\Controller\Plugin\GetAccessLevel'
)
),
Now every controller will have access to this plugin.
Some Controller
public function someAction() {
$accessLevel = $this->getAccesslevel(array('User Role Entities Go Here'));
}

Resources