Ever since I started using Zend Framework 3, I had problems with testing my controllers. I'm trying to test my controllers with PhpUnit 5.7 and my controllers depend on Zend Form, which is hydrated with Doctrine's DoctrineObject.
I'm trying to put this as simple as possible, so here's a minimal example of a setup that's giving me headaches:
Controller:
class IndexController extends AbstractActionController {
private $form;
public function __construct(AlbumForm $form) {
$this->form = $form;
}
public function indexAction() {
return ['form' => $this-form];
}
}
ControllerFactory:
class IndexControllerFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, ...) {
$formManager = $container->get('FormElementManager');
return new IndexController($formManager->get(AlbumForm::class));
}
}
The corresponding view template in albums/index/index.phtml:
<?php
$this->form->prepare();
$this->form->setAttribute('action', $this->url(null, [], true));
$albumFieldset = $this->form->get('album');
?>
<?= $this->form()->openTag($this-form) ?>
<div class="form-group">
<?= $this->formRow($albumFieldset->get('name')) ?>
</div>
<?= $this->form()->closeTag() ?>
The form:
class AlbumForm extends Form {
public function init() {
$this->add([
'name' => 'albumFieldset',
'type' => AlbumFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
}
}
The fieldset:
class AlbumFieldset extends Fieldset {
public function init() {
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name of album',
],
]);
}
}
The FieldsetFactory:
class AlbumFieldsetFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, ...) {
$objectManager = $container->get(ObjectManager::class);
$fieldset = new AlbumFieldset();
$fieldset->setHydrator(new DoctrineObject($objectManager));
$fieldset->setObject(new Album());
return $fieldset;
}
}
Now, so far everything is working great.
However, when writing tests for this I run into troubles. Let me first show you what I have so far:
class IndexControllerTest extends AbstractHttpControllerTestCase {
protected function setUp() {
parent::setUp();
$this->configureServiceManager($this->getApplicationServiceLocator());
}
private function configureServiceManager(ServiceManager $services) {
$services->setAllowOverride(true);
$services->setService(ObjectManager::class, $this->mockObjectManager()->reveal());
$services->setService('FormElementManager', $this->mockFormManager()->reveal());
$services->setAllowOverride(false);
}
private $objectManager;
private function mockObjectManager() {
$this->objectManager = $this->prophesize(ObjectManager::class);
return $this->objectManager;
}
private $formManager;
private function mockFormManager() {
$this->formManager = $this->prophesize(FormElementManager::class);
$this->formManager->get(AlbumForm::class)->willReturn($this->mockForm()->reveal());
return $this->formManager;
}
private $form;
private function mockForm() {
$this->form = $this->prophesize(AlbumForm::class);
$this->form->prepare()->willReturn(null);
$this->form->setAttribute('action', Argument::type('string'))->willReturn(null);
$this->form->getAttributes()->willReturn([]);
$this->form->get('album')->willReturn($this->mockAlbumFieldset()->reveal());
return $this->form;
}
private $albumFieldset;
private function mockAlbumFieldset() {
$this->albumFieldset = $this->prophesize(AlbumFieldset::class);
$this->albumFieldset->get('name')->willReturn($this->mockName()->reveal());
return $this->albumFieldset;
}
private $name;
private function mockName() {
$this->name = $this->prophesize(Text::class);
$this->name->getLabel()->willReturn('label');
$this->name->getLabelAttributes()->willReturn(['for' => 'name']);
$this->name->getLabelOption('disable_html_escape')->willReturn(false);
$this->name->getLabelOption('always_wrap')->willReturn(false);
$this->name->getLabelOption('label_position')->willReturn('prepend');
$this->name->getName('album[name]');
$this->name->getAttribute('type')->willReturn('text');
$this->name->hasAttribute('id')->willReturn(true);
$this->name->getAttribute('id')->willReturn('name');
$this->name->getAttributes([])->willReturn([]);
$this->name->getValue()->willReturn(null);
$this->name->getMessages()->willReturn([]);
return $this->name;
}
}
This will eventually run without errors. However, I would like to draw your attention to the last few methods, especially mockName(). Most of those definitions are totally default and almost none of them are specified in AlbumFieldset in the beginning (only name is). It is very annoying to write them down for every form input I may have and writing this down actually introduces more errors than it solves. For example, I'm still not sure what the correct label option for always_wrap would be. I actually don't even care about that option, but I have to write something about it in my test, because otherwise the test fails with 'Prophecy\Exception\Call\UnexpectedCallException' with message 'Method call: - getLabelOption("always_wrap") on Double\Zend\Form\Element\Text\P245 was not expected, expected calls were: ....
Therefore, I'm asking you: is there any better way to go about this? A way that does not involve writing 20+ rows for every field I have in my fieldset. If it involves rewriting my controllers/fieldsets/view templates (etc.), that would totally be fine!
Any help is greatly appreciated! Also, this is my very first time asking something in a forum in over eight years of programming, so please bear with me if anything is unclear.
Yours
Steffen
PS: What I have already tried is to give the IndexController null instead of an actual form and simply abort the view template when it detects that the form is null. However, while that worked without that much setup, I was basically just avoiding the view template's logic. Because of that, I was not able to detect errors in the view template. That's not what I want.
edit IndexControllerTest: Change private as protected elsewhere and extends it for your new fields. Each new controller must overwrite methods calling parent::methodname($args) and add the needed code...
Related
I'm looking for a way to extend Symfony 2 EntityType
Symfony\Bridge\Doctrine\Form\Type\EntityType
as in a new type extending this one, not creating a FormTypeExtension - and I can't figure it out. Does anyone know any proper way to do that?
I've tried simply extending it that way:
class NestedEntityType extends EntityType {
public function getName() {
return $this->getBlockPrefix();
}
public function getBlockPrefix() {
return 'nested_entity';
}
}
and then in sonata admin class I have:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('types', NestedEntityType::class, [
'label' => false,
'multiple' => true,
'expanded' => true,
'by_reference' => false
]);
}
but unfortunately it causes Fatal Error:
Catchable Fatal Error: Argument 1 passed to
Symfony\Bridge\Doctrine\Form\Type\DoctrineType::__construct() must
implement interface Doctrine\Common\Persistence\ManagerRegistry, none
given, called in
I need to keep the whole functionality of EntityType, with one exception - the way it's presented. That's why I need to extend this type (I use it in other fields, so I can't just modify the template for it!).
I'm using Symfony 2.8 (just for the record).
You should not extend it directly but use parent option
/**
* {#inheritdoc}
*/
public function getParent()
{
return EntityType::class;
}
So something like
class NestedEntityType extends AbstractType
{
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'nested_entity';
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return EntityType::class;
}
}
That way if FormType you're extending from has something in injected (or setted) into constructor, you don't need to care as symfony will do it for you.
So, if you need a create a reusable solution, for different Entities, you don't need a configureOptions in this case.
You need to create an elementType in you code like
$this->createForm(NestedEntityType::class, null, ['class' => YourEntity::class]);
And in this case, you will need to pass as an option a name of class Entity which is nested.
If you would go to EntityType, you'll see it's extending DoctrineType and needs dependencies in constructor -
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
So, your class NestedEntityType uses the same constructor and needs the same dependencies.
Actually, what you need is
class NestedEntityType extends AbstractType {
public function getParent() {
return EntityType::class;
}
public function getName() {
return $this->getBlockPrefix();
}
public function getBlockPrefix() {
return 'nested_entity';
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => YourEntity::class
]);
}
}
UPD: Of course you need configure options, according to EntityType doc. See method I've added to code.
Hi everyone and happy Easter !
I have this problem in migrating a legacy application from CakePHP 2.x to CakePHP 3.x
I am trying to load all the contents of a database table in the initialize function of the model (ConfigurationsTable.php - made into a Singleton) class. I also tried the same code in the constructor but still get the same error. Also tried moving it to a separate function but still no luck.
It works fine in CakePHP 2.x but I get a fatal error in CakePHP 3.
Code is as follows
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class ConfigurationsTable extends Table
{
private $_configurations;
public static function getInstance()
{
static $instance = null;
if ($instance === null) {
$instance = new static();
}
return $instance;
}
public function is_set($key)
{
return isset($this->_configurations->{$key});
}
public function fetch($key)
{
return $this->_configurations->{$key};
}
public function initialize(array $config)
{
$this->addBehavior('Timestamp');
$this->_configurations = new \stdClass();
$configs = $this->find('all');
foreach ($configs as $c) {
if (isset($c->key) && $c->key != '') {
$this->_configurations->{$c->key} = $c->value;
}
}
}
public function validationDefault(Validator $validator)
{
$validator
->notEmpty('key')
->add('key', [
'unique' => [
'rule' => 'validateUnique',
'provider' => 'table',
'message' => __('This configuration key already exists')
]
])
->notEmpty('value')
;
return $validator;
}
The line that's causing the error is: $configs = $this->find('all');
Can anyone provide me a solution for this ?
I need it for work..
Thanks a lot in advance
You cannot blindly apply 2.x concepts and expect it to work. Before you start working with code that you don't know about, take at least a look at the API docs
http://api.cakephp.org/3.0/class-Cake.ORM.Table.html#___construct
and having a look at the source itself to understand what the code actually does would be even better.
When manually instantiating table classes, you must at least provide a connection instance. However, what you are doing there doesn't make much sense, there's not really a need for a custom static instance getter, that's what TableRegistry::get() is there for.
I need to programmatically change the behaviour of a form based on some options. Let's say, for example, I'm displaying a form with some user's info.
I need to display a checkbox, "send mail", if and only if a user has not received an activation mail yet. Previously, with ZF1, i used to do something like
$form = new MyForm(array("displaySendMail" => true))
which, in turn, was received as an option, and which allow'd to do
class MyForm extends Zend_Form {
protected $displaySendMail;
[...]
public function setDisplaySendMail($displaySendMail)
{
$this->displaySendMail = $displaySendMail;
}
public function init() {
[....]
if($this->displaySendMail)
{
$displaySendMail new Zend_Form_Element_Checkbox("sendmail");
$displaySendMail
->setRequired(true)
->setLabel("Send Activation Mail");
}
}
How could this be accomplished using Zend Framework 2? All the stuff I found is about managing dependencies (classes), and nothing about this scenario, except this SO question: ZF2 How to pass a variable to a form
which, in the end, falls back on passing a dependency. Maybe what's on the last comment, by Jean Paul Rumeau could provide a solution, but I wasn't able to get it work.
Thx
A.
#AlexP, thanks for your support. I already use the FormElementManager, so it should be straightforward. If I understand correctly, I should just retrieve these option in my SomeForm constructor, shouldn't I?
[in Module.php]
'Application\SomeForm' => function($sm)
{
$form = new SomeForm();
$form->setServiceManager($sm);
return $form;
},
while in SomeForm.php
class SomeForm extends Form implements ServiceManagerAwareInterface
{
protected $sm;
public function __construct($name, $options) {
[here i have options?]
parent::__construct($name, $options);
}
}
I tryed this, but was not working, I'll give it a second try and double check everything.
With the plugin managers (classes extending Zend\ServiceManager\AbstractPluginManager) you are able to provide 'creation options' array as the second parameter.
$formElementManager = $serviceManager->get('FormElementManager');
$form = $formElementManager->get('SomeForm', array('foo' => 'bar'));
What is important is how you have registered the service with the manager. 'invokable' services will have the options array passed into the requested service's constructor, however 'factories' (which have to be a string of the factory class name) will get the options in it's constructor.
Edit
You have registered your service with an anonymous function which mean this will not work for you. Instead use a factory class.
// Module.php
public function getFormElementConfig()
{
return array(
'factories' => array(
'Application\SomeForm' => 'Application\SomeFormFactory',
),
);
}
An then it's the factory that will get the options injected into it's constructor (which if you think about it makes sense).
namespace Application;
use Application\SomeForm;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class SomeFormFactory implements FactoryInterface
{
protected $options = array();
public function __construct(array $options = array())
{
$this->options = $options;
}
public function createService(ServiceLocatorInterface $serviceLocator)
{
return new SomeForm('some_form', $this->options);
}
}
Alternatively, you can inject directly into the service you are requesting (SomeForm) by registering it as an 'invokeable' service; obviously this will depend on what dependencies the service requires.
I am using Laravel and it's Validators.
I have the following code in my controller:
class ResellerController extends BaseController{
public function add() {
//some code before
$userValidator = new App\Services\Validators\UserCreateValidator();
//HERE I WANT TO REMOVE THE company KEY FROM THE RULES IN THE USERS CREATE VALIDATOR
$userValidator->removeRule('company');
//code execution continues
}
}
The UserCreateValidator extends a parent Validator class:
namespace App\Services\Validators;
class UserCreateValidator extends Validator {
public static $rules = array(
'firstName' => 'required',
'lastName' => 'required',
'email' => 'required|email|unique:users',
'company' => 'required'
);
}
And here is the base Validator class:
namespace App\Services\Validators;
abstract class Validator {
/**
* Validation rules
* #var array
*/
public static $rules;
//THIS CODE DOES NOT WORK IN THE CONTROLLER UP
public static function removeRule($ruleKey){
if(is_array($ruleKey))
{
foreach($ruleKey as $key)
{
if(!array_key_exists($key, static::$rules)) continue;
unset(static::$rules[$key]);
}
return true;
}
if(!array_key_exists($ruleKey, static::$rules)) //return false;
unset(static::$rules['company']);
return true;
}
}
The unsettting of the static::$rules[$key] in ResellerController does not work.
I can see in a XDEBUG session (after this line gets executed) that the static::$rules['company'] is still present in the UserCreateValidator as property.
I thought that Late Static Binding should solve this problem?
What is wrong?
The problem is solved. It was in the commented part in the:
if(!array_key_exists($ruleKey, static::$rules)) //return false;
The unsetting is working fine after I uncomment the return false.
Silly mistake :)
I have custom router and I have to get access to Zend\Navigation inside this custom router. I was googling, asking and searching and no results :/
All I need is to find nodes with 'link' param using Zend\Navigation in my Alias::match function.
Here is my module.config.php:
'navigation' => array(
'default' => array(
'account' => array(
'label' => 'Account',
'route' => 'node',
'pages' => array(
'home' => array(
'label' => 'Dashboard',
'route' => 'node',
'params' => array(
'id' => '1',
'link' => '/about/gallery'
),
),
),
),
),
),
[...]
And here is my Alias class:
// file within ModuleName/src/ModuleName/Router/Alias.php
namespace Application\Router;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Mvc\Router\Http;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class Alias extends Http\Segment implements ServiceLocatorAwareInterface
{
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function match(Request $request, $pathOffset = null)
{
[...]
return parent::match($request, $pathOffset);
}
}
EDITED:
Now i know that I should inject service manager into my custom router. Let me know if You know how to do this :)
EDITED:
Ok, its not custom router but route. My bad. I was talking on #zftalk irc chanell and AliasSegment class should implements ServiceLocatorAwareInterface. Ok I've tried it but now there is another problem.
In setServiceLocator function i can't get service locator. It returns null object, however $serviceLocator is class Zend\Mvc\Router\RoutePluginManager.
public function setServiceLocator(ServiceLocatorInterface $serviceLocator){
$sl = $serviceLocator->getServiceLocator();
var_dump($sl); // NULL
}
Any ideas how to get Zend navigation from it ?
EDITED
Corresponding to what #mmmshuddup said, I've changed my custom router class. (New version is above). Also in my Module.php, within onBootstrap function, I added this line:
$sm->setFactory('Navigation', 'Zend\Navigation\Service\DefaultNavigationFactory', true);
Navigation works and its instantiated before route so it should be visible within my Alias class but it's not.
I've put into my match function in Alias class this line:
$servicesArray = $this->getServiceLocator()->getRegisteredServices();
and $servicesArray is almost empty. There is no service, no factories. The same line inserted into onBootstrap, just after setting new factory (as above) returns array with navigation and other services.
The question is: how can i share this array (or ServiceManager) with my custom router: Alias ?
I have to say that all I want to do was possible in ZF1 and it was quite easy.
EDIT
I found a solution. The answer is below
That is because the object itself really doesn't have any properties declared. But if you do this:
echo get_class($sl);
You will see that it is indeed an instance of Zend\ServiceManager\ServiceManager
You should be able to get your navigation instance by doing something like:
$nav = $sl->get('Navigation');
EDIT:
I just notice you have some stuff in the wrong location of your code. You're calling getServiceLocator() on $serviceLocator which is already the instance of that. Also you're calling it within setServiceLocator(). You should change it to:
// EDIT - file within ModuleName/src/Router/Alias.php
namespace Application\Router;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Mvc\Router\Http;
class Alias extends Http\Segment implements ServiceLocatorAwareInterface
{
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function match(Request $request, $pathOffset = null)
{
$nav = $this->getServiceLocator()->get('Navigation');
// ...
return parent::match($request, $pathOffset);
}
}
I found the solution but this is NOT elegant solution i think. However everything works perfectly. If somebody knows disadvantages of this solution, please comment this answer or add another, better. I had to modify #mmmshuddup's idea (you can read the conversation).
First of all, the implementation of ServiceLocatorAwareInterface in custom route class is no more necessary.
In Module.php within onBootstrap function:
$app = $e->getApplication();
$sm = $app->getServiceManager();
$sm->get('translator');
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$sm->setFactory('Navigation',
'Zend\Navigation\Service\DefaultNavigationFactory', true);
$nav = $sm->get('Navigation');
$alias = $sm->get('Application\Router\Alias');
$alias->setNavigation($nav);
First we instantiate Navigation factory in ServiceManager and then our custom route. After that we can pass Navigation class into custom route using setNavigation function.
To complete instantiate of our custom route we need in getServiceConfig in the same file:
return array(
'factories' => array(
'Application\Router\Alias' => function($sm) {
$alias = new \Application\Router\Alias('/node[/:id]');
return $alias;
},
'db_adapter' => function($sm) {
$config = $sm->get('Configuration');
$dbAdapter = new \Zend\Db\Adapter\Adapter($config['db']);
return $dbAdapter;
},
)
);
And here is a tricky part. This instance is temporary. While routing, this class will be instantiated one more time and this is why, I think, it's not very elegant. We have to insert parameter into constructor however at this moment value of this parameter is not important.
The custom route class:
// file within ModuleName/src/ModuleName/Router/Alias.php
namespace Application\Router;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Mvc\Router\Http;
class Alias extends Http\Segment
{
private static $_navigation = null;
public function match(Request $request, $pathOffset = null)
{
//some logic here
//get Navigation
$nav = self::$_navigation;
return parent::match($request, $pathOffset);
}
public function setNavigation($navigation){
self::$_navigation = $navigation;
}
}
Because first instance is temporary, we have to collect our Navigation class in static variable. It's awful but works nice. Maybe there is a way to instantiate it only once and in route configuration get instance of it, but at this moment this is best answer for my question. Simply enough and working correctly.