I'd like to be able to alter a form using the Symfony Event Dispatcher. The Symfony documentation talks about dynamic form modification, however, in all examples the event listener or subscriber is created in the form class. I'd like to keep this logic decoupled from my form class.
How can I modify a Symfony form without having to specify which event listeners are going to be called in the form class?
Probably what you need is a form type extension, it allow you to modify any existing form types across the entire system. There, you can add event listeners/subscribers or what you want to any specific or generic form type.
However, this task tends to get tedious if it's a very frequent case. So doing something like this can provide you with a perfect fit:
class FoobarFormSubscriber implements EventSubscriberInterface, FormEventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public static function getFormClass()
{
return FoobarType::class;
}
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$form->add('custom', null, array('mapped' => false));
}
}
But obviously this isn't a feature implemented by Symfony. Here I leave you a recipe to achieve it:
First, create a new form type extension to add the subscriber to the form builder according to configuration:
class FormEventTypeExtension extends AbstractTypeExtension
{
private $subscribers;
public function __construct(array $subscribers = array())
{
$this->subscribers = $subscribers;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$formClass = get_class($builder->getType()->getInnerType());
if (isset($this->subscribers[$formClass])) {
foreach ($this->subscribers[$formClass] as $subscriber) {
$builder->addEventSubscriber($subscriber);
}
}
}
public function getExtendedType()
{
return FormType::class;
}
}
Create a new interface to configure the form class to listen to:
interface FormEventSubscriberInterface
{
public static function getFormClass();
}
Finally, into a new compiler pass, injects to the extension service all registered kernel.event_subscriber that implement the previous interface:
public function process(ContainerBuilder $container)
{
$subscribers = array();
foreach ($container->findTaggedServiceIds('kernel.event_subscriber') as $serviceId => $tags) {
$subscriberClass = $container->getDefinition($serviceId)->getClass();
if (is_subclass_of($subscriberClass, FormEventSubscriberInterface::class, true)) {
$subscribers[$subscriberClass::getFormClass()][] = new Reference($serviceId);
}
}
$extensionDef = $container->getDefinition(FormEventTypeExtension::class);
$extensionDef->setArgument(0, $subscribers);
}
Then, your custom subscribers are decoupled and ready to work as is, just make sure to implement both interfaces (EventSubscriberInterface, FormEventSubscriberInterface) and register the event subscriber as service.
Related
My goal is it to add a custom FormAction to a DataObjects EditForm inside a ModelAdmin, next to the "Save" and "Delete" action.
The setup:
With updateCMSActions I can add that button.
class MyActionEventExtension extends DataExtension {
public function updateCMSActions(FieldList $actions) {
if($this->owner->canEdit(Member::currentUser())) {
$actions->push(FormAction::create('doMyAction', 'Action'));
}
return $actions;
}
}
This works perfectly fine.
Following this answer on stackoverflow I created a LeftAndMainExtension for the actions handler.
class MyActionLeftAndMainExtension extends LeftAndMainExtension {
private static $allowed_actions = array(
'doMyAction'
);
public function doMyAction($data, $form) {
// Do stuff.
// ... I never get here.
// Return stuff
$this->owner->response->addHeader(
'X-Status',
rawurlencode('Success message!')
);
return $this->owner->getResponseNegotiator()
->respond($this->owner->request);
}
}
The corresponding config.yml file looks like this:
LeftAndMain:
extensions:
- MyActionLeftAndMainExtension
TheDataObject:
extensions:
- MyActionEventExtension
The Problem:
When I click the button, the response gives me "404 Not Found".
The requested URL always is the same:
http://localhost/admin/model-admin-url-slug/TheDataObject/EditForm/field/TheDataObject/item/878/ItemEditForm
Some other solutions I found, suggested to extend the ModelAdmins GridField. This sadly is not an option, since the DataObject I need that action for has alot of relations, which means, it's EditForm also appears in other DataObjects EditForms (nested).
I'm really running out of ideas. Did I miss something within my ModelAdmin? The one I created only implements the basic static vars, so I didn't posted it here.
Any help would be great!
Update:
I ended up, providing a getEditForm method on my ModelAdmin.
public function getEditForm($id = null, $fields = null) {
$form = parent::getEditForm($id, $fields);
$listField = $form->Fields()->fieldByName($this->modelClass);
if ($gridField = $listField->getConfig()->getComponentByType('GridFieldDetailForm')) {
$gridField->setItemRequestClass('MyAdminForm_ItemRequest');
}
return $form;
}
and extending the GridFieldDetailForm_ItemRequest:
class MyAdminForm_ItemRequest extends GridFieldDetailForm_ItemRequest {
private static $allowed_actions = array (
'edit',
'view',
'ItemEditForm'
);
public function ItemEditForm() {
$form = parent::ItemEditForm();
$formActions = $form->Actions();
// Adds all FormActions provided by the model's `getCMSActions` callback
if ($actions = $this->record->getCMSActions()) {
foreach ($actions as $action) {
$formActions->push($action);
}
}
return $form;
}
public function doAction($data, $form) {
// do stuff here
}
}
Sadly this doesn't add an action on has_many or many_many relation gridfields.. and because of that, I'll leave the question opened and unanswered. Maybe sometime there will be a better solution.. :)
One very simple answer to this question (if it's an option for you) is to use the Better Buttons module: https://github.com/unclecheese/silverstripe-gridfield-betterbuttons#creating-a-custom-action
It lets you define the actions on the model, which is a bit questionable from an architecture standpoint, but also works pretty well in the context of Silverstripe and ModelAdmin.
I am trying to check in the constructor of a model if the currently authenticated user is allowed to access the given model, but I am finding that $this from the constructor's context is empty. Where are the attributes assigned to a model in Laravel and how should I go about calling a method once all of the attributes have been loaded?
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
var_dump($this); // empty model
$this->checkAccessible();
}
Cheers in advance
As stated in the other answers & comments, there are better ways to achieve the aims of the question (at least in modern Laravel). I would refer in this case to the Authorization chapter of the documentation that goes through both gates and policies.
However, to answer the specific question of how to call a method once a models attributes have been loaded - you can listen for the Eloquent retrieved event. The simplest way to do this within a class is using a closure within the class booted() method.
protected static function booted()
{
static::retrieved(function ($model) {
$model->yourMethod() //called once all attributes are loaded
});
}
You can also listen for these events in the normal way, using listeners. See the documentation for Eloquent events.
you can use controller filter to check whether user logged in or not and than you call any model function.
public function __construct(array $attributes = []){
$this->beforeFilter('auth', array('except' => 'login')); //login route
if(Auth::user()){
$user_id = Auth::user()->user_id;
$model = new Model($attributes);
//$model = User::find($user_id);
}
}
Binding Attributes to Model from constructor
Model.php
public function __construct(array $attributes = array())
{
$this->setRawAttributes($attributes, true);
parent::__construct($attributes);
}
As it was mentioned by Rory, the retrieved event is responsible for that.
Also, it could be formed in a much cleaner and OOP way with Event/Listener approach, especially if you need to write a lot of code or have few handlers.
As it described here, you can just create an event for the Model like
protected $dispatchesEvents = [
'retrieved' => UserLoaded::class,
];
You need to create this class, eloquent event accepts the model by default:
class UserLoaded
{
protected User $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
Then here is described how to declare listener for this event. It should be somewhere in the EventListenerProvider like this:
protected $listen = [
UserLoaded::class => [
UserLoadedListener::class
],
];
The listener should just implement method handle() (check article) like:
public function handle(UserLoaded $event)
{
// your code
}
Another possibility is to register model Observer, as it´s described here
I am developing a web application that features a timeline, much like a Facebook timeline. The timeline itself is completely generic and pluggable. It's just a generic collection of items. Anything with the proper interface (Dateable) can be added to the collection and displayed on the timeline.
Other components (Symfony bundles) define models that implement the Dateable interface and set up a provider that can find and return these models. The code is much like this:
class Timeline
{
private $providers = []; // Filled by DI
public function find(/* ... */)
{
$result = [];
foreach ($this->providers as $provider) {
$result = array_merge($result, $provider->find(/* ... */));
}
return $result;
}
}
The problem is that there needs to be a set of filters next to the timeline. Some filter options (like date) apply to all providers. But most options do not. For example, most providers will be able to use the author filter option, but not all. Some notification item are dynamically generated and don't have an author.
Some filter options just apply to a single provider. For example, only event items have a location property.
I can't figure out how to design a filter form that is just as modular as the timeline itself. Where should the available filter options be defined? Bundle-specific filter options could probably come from the bundle itself, but how about filter options (like user) that can be used by multiple bundles? And what if some filter options later become usable by multiple bundles? For example, only events have a location now, but what if another module is added that also has items with a location?
And how will each provider determine if the submitted filter form only contains options it understands? If I set a location in the filter then the BlogPostProvider should not return any messages, because blog posts have no location. But I can't check for location in the filter because the BlogPostBundle shouldn't know about other providers and their filtering options.
Any ideas on how I can design such a filter form?
Add a central FilterHandler where each available filter can be registered. Generic filters can be kept in the same bundle as the handler and registered from there and bundles can also register filters.
All providers should know if and when which filters they use (by the filters name). Also DI the handler in them.
From the handler you can get the complete list of registered filters and then build your filter form upon this.
When filtering call $provider->filter($requestedFiltersWithValues) which will check if the filters it uses are actually requested and registered (via the injected handler) and return results as needed.
This is how I solved it in the end.
First off, I have a FilterRegistry. Any bundle can add filters to it using a Symfony DI tag. A filter is simply a form type. Example filter:
class LocationFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('location', 'choice', [ /* ... */ ]);
}
}
DI configuration:
<service id="filter.location" class="My\Bundle\Form\LocationFilterType">
<tag name="form.type" alias="filter_location" />
<tag name="filter.type" alias="location" />
</service>
The FilterRegistry knows how to get those form types from the DI container:
class FilterRegistry
{
public function getFormType($name)
{
if (!isset($this->types[$name])) {
throw new \InvalidArgumentException(sprintf('Unknown filter type "%s"', $name));
}
return $this->container->get($this->types[$name]);
}
}
The Timeline class and the providers use the FilterBuilder to add new filters to the filter form. The builder looks like this:
class FilterBuilder
{
public function __construct(FilterRegistry $filterRegistry, FormBuilderInterface $formBuilder)
{
$this->filterRegistry = $filterRegistry;
$this->formBuilder = $formBuilder;
}
public function add($name)
{
if ($this->formBuilder->has($name)) {
return;
}
$type = $this->filterRegistry->getFormType($name);
$type->buildForm($this->formBuilder, $this->formBuilder->getOptions());
return $this;
}
}
In order to display the form, a filter is build using the options of all providers. This happens in Timeline->getFilterForm(). Note that there is no data object tied to the form:
class Timeline
{
public function getFilterForm()
{
$formBuilder = $this->formFactory->createNamedBuilder('', 'base_filter_type');
foreach ($this->providers as $provider) {
$provider->configureFilter(new FilterBuilder($this->filterRegistry, $formBuilder));
}
return $formBuilder->getForm();
}
}
Each provider implements the configureFilter method:
class EventProvider
{
public function configureFilter(FilterBuilder $builder)
{
$builder
->add('location')
->add('author')
;
}
}
The find method of the Timeline class is also modified. Instead of building a filter with all options, it builds a new filter form with just the options for that provider. If form validation fails then the provider cannot handle the currently submitted combination of filters. Usually this is because a filter option is set that the provider does not understand. In that case, form validation fails due to extra data being set.
class Timeline
{
public function find(Request $request)
{
$result = [];
foreach ($this->providers as $provider) {
$filter = $provider->createFilter();
$formBuilder = $this->formFactory->createNamedBuilder('', 'base_filter_type', $filter);
$provider->configureFilter(new FilterBuilder($this->filterRegistry, $formBuilder));
$form = $formBuilder->getForm();
$form->handleRequest($request);
if (!$form->isSubmitted() || $form->isValid()) {
$result = array_merge($result, $provider->find($filter));
}
}
return $result;
}
}
In this case, there is a data class tied to the form. $provider->createFilter() simply returns an object that has properties matching the filters. The filled and validated filter object is then passed to the provider's find() method. E.g:
class EventProvider
{
public function createFilter()
{
return new EventFilter();
}
public function find(EventFilter $filter)
{
// Do something with $filter and return events
}
}
class EventFilter
{
public $location;
public $author;
}
This whole thing together makes it pretty easy to manage the filters.
To add a new type of filter:
Implement a FormType
Tag it in the DI as a form.type and as a filter.type
To start using a filter:
Add it to the FilterBuilder in configureFilters()
Add a property to the filter model
Handle the property in the find() method
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.
assumption: Event\Service\EventService is my personal object that works with Event\Entity\Event entities
This code works in an ActionController:
$eventService = $this->getServiceLocator()->get('Event\Service\EventService');
How can I get $eventService in a Zend\Form\Form in the same way?
You have two options if you have a dependency like this. In your case, a Form depends on a Service. The first option is to inject dependencies:
class Form
{
protected $service;
public function setService(Service $service)
{
$this->service = $service;
}
}
$form = new Form;
$form->setService($service);
In this case, the $form is unaware of the location of $service and generally accepted as a good idea. To make sure you don't need to set up all the dependencies yourself each time you need a Form, you can use the service manager to create a factory.
One way (there are more) to create a factory is to add a getServiceConfiguration() method to your module class and use a closure to instantiate a Form object. This is an example to inject a Service into a Form:
public function getServiceConfiguration()
{
return array(
'factories' => array(
'Event\Form\Event' => function ($sm) {
$service = $sm->get('Event\Service\EventService');
$form = new Form;
$form->setService($service);
return $form;
}
)
);
}
Then you simply get the Form from your service manager. For example, in your controller:
$form = $this->getServiceLocator()->get('Event\Form\Event');
A second option is to pull dependencies. Though it is not recommended for classes like forms, you can inject a service manager so the form can pull dependencies itself:
class Form
{
protected $sm;
public function setServiceManager(ServiceManager $sm)
{
$this->sm = $sm;
}
/**
* This returns the Service you depend on
*
* #return Service
*/
public function getService ()
{
return $this->sm->get('Event\Service\EventService');
}
}
However, this second option couples your code with unnecessary couplings and it makes it very hard to test your code. So please use dependency injection instead of pulling dependencies yourself. There are only a handful of cases where you might want to pull dependencies yourself :)
You can just configure the form with all the options in the module.php. In the following code I:
Name the service as my_form
Associate the new object \MyModule\Form\MyForm with this service
Inject the service 'something1' to the _construct()
Inject the service 'something2' to the setSomething()
Code:
public function getServiceConfiguration()
{
return array(
'factories' => array(
'my_form' => function ($sm) {
$model = new \MyModule\Form\MyForm($sm->get('something1'));
$obj = $sm->get('something2');
$model->setSomething($obj);
return $model;
},
),
);
}
And then in the controller the following line will populate your object with all needed dependencies
$form = $this->getServiceLocator()->get('my_form');
Use the form element manager to get the form in your controller:
$form = $this->getServiceLocator()->get('FormElementManager')->get('Path\To\Your\Form', $args);
Then in your form will become this
<?php
namespace Your\Namespace;
use Zend\Form\Form;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ ServiceLocatorAwareTrait;
class MyForm extends Form implements ServiceLocatorAwareInterface {
use ServiceLocatorAwareTrait;
public function __construct($class_name, $args)
{
/// you cannot get the service locator in construct.
}
public function init()
{
$this->getServiceLocator()->get('Path\To\Your\Service');
}
}