Zend Framework 2: Extend ZfcUser with own fields - php

I am working on my first Zend Framework 2 Project. I needed a User Module and integrated ZfcUser for this. Because I have a slight difference in my User Table, I had to use my own User Entity and User Mapper. I created a new Module called ZfcUserExtension.
I then copied a lot of files from the original ZfcUSer Module like:
Entity/User.php
Entity/UserInterface.php
Factory/Entity/IndexControllerFactory.php
Factory/Mapper/UserHydratorFactory.php
Mapper/Exeption/ExceptionInterface
Mapper/Exeption/InvalidArgumentException.php
Mapper/Exeption/RuntimeException.php Mapper/HydratorInterface.php
Mapper/User.php Mapper/UserHydrator.php Mapper/UserHydrator.php
Mapper/UserInterface.php
In zfcuser.global.php I set the user_entity_class to use my own Entity.
'user_entity_class' => 'ZfcUserExtension\Entity\User',
In the module.config.php from the ZfcUserExtension I add the below to make sure that I use my own User Mapper and UserHydrator. The reason for that was that I use "id" as a Primary Key in my User table instead of "user_id", so I had to make sure that this gets overwritten as well.
<?php
return array(
'controllers' => array(
'factories' => array(
'ZfcUserExtension\Controller\Index' => function(Zend\Mvc \Controller\ControllerManager $cm) {
$sm = $cm->getServiceLocator();
return new \ZfcUserExtension\Controller\IndexController(
$sm->get("doctrine.entitymanager.orm_default")
);
}
),
),
'service_manager' => array(
'factories' => array(
'zfcuser_user_mapper' => function ($sm) {
$options = $sm->get('zfcuser_module_options');
$mapper = new \ZfcUserExtension\Mapper\User();
// No db adapter present add below line
$mapper->setDbAdapter($sm->get('zfcuser_zend_db_adapter'));
$entityClass = $options->getUserEntityClass();
// No entity prototype set add below line
$mapper->setEntityPrototype(new $entityClass);
$mapper->setHydrator($sm->get('zfcuser_user_hydrator'));
$mapper->setTableName($options->getTableName());
return $mapper;
},
// 'zfcuserextension_change_password_form' => 'ZfcUserExtension\Factory\Form\ChangePhoneFormFactory',
),
),
I finally got all this to work, till I now run into another problem. I want some additional fields for the User like Phone Number. How would I approach this? I know there are some ideas on the Internet, but I am mainly interested to know how I would actually offer the option to have a "Change Phone" Form. I have created a Form, similar to the "Change Password and "Change Email". I have then created a IndexController.php in my ZfcUSerExtension, again followed the set-up of the UserController from the ZfcUser Module
class IndexController extends AbstractActionController {
const ROUTE_LOGIN = 'zfcuser/login';
/**
* #var \Doctrine\ORM\EntityManager
*/
protected $em;
public function __construct(\Doctrine\ORM\EntityManager $em)
{
$this->em = $em;
}
/**
* #var Form
*/
protected $changeEmailForm;
public function indexAction() {
if (!$this->zfcUserAuthentication()->hasIdentity()) {
return $this->redirect()->toRoute(static::ROUTE_LOGIN);
}
return new ViewModel();
}
public function changephoneAction() {
// if the user isn't logged in, we can't change phone
if (!$this->zfcUserAuthentication()->hasIdentity()) {
return $this->redirect()->toRoute(static::ROUTE_LOGIN);
}
$form = $this->getChangePhoneForm();
$request = $this->getRequest();
$request->getPost()->set('PrevPhone', $this->getUserService()->getAuthService()->getIdentity()->getPrevPhone());
return array(
'status' => false,
'changePhoneForm' => $form,
);
$fm = $this->flashMessenger()->setNamespace('change-phone')->getMessages();
if (isset($fm[0])) {
$status = $fm[0];
} else {
$status = null;
}
$prg = $this->prg(static::ROUTE_LOGIN);
if ($prg instanceof Response) {
return $prg;
} elseif ($prg === false) {
return array(
'status' => $status,
'changePhoneForm' => $form,
);
}
$form->setData($prg);
if (!$form->isValid()) {
return array(
'status' => false,
'changePhoneForm' => $form,
);
}
$change = $this->getUserService()->changeEmail($prg);
if (!$change) {
$this->flashMessenger()->setNamespace('change-email')->addMessage(false);
return array(
'status' => false,
'changeEmailForm' => $form,
);
}
$this->flashMessenger()->setNamespace('change-email')->addMessage(true);
return $this->redirect()->toRoute(static::ROUTE_CHANGEEMAIL);
}
public function getChangePhoneForm()
{
$sl = $this->getServiceLocator();
$this->setChangePhoneForm($sl->get('zfcuserextension_change_phone_form'));
return $this->changePhoneForm;
}
public function setChangePhoneForm($changePhoneForm)
{
$this->changePhoneForm = $changePhoneForm;
return $this;
}
I now noticed that I will face a problem with the User Service Service/User.php. The Service offers a changePassword() and changeEmail() Method. I now thought that I need to copy this file into my own Modules. Am I right that if I extend the User Service from ZfcUser then the Methods changePassword() and changeEmail() will still be available, so I would delete it from the just copied file and just add changePhone()?
And if I am right with my thoughts, the User Service currently starts like this:
class User extends EventProvider implements ServiceManagerAwareInterface
How would I have to change it that I extend the original User Service? I hope somebody can help, I am still rather confused with all this. Thanky you very much in advance.

There are two possible methods:
Build custom classes extending ZfcUser's entity, form and input filter and add your custom fields. In the ZfcUser configuration change aliases or override factories to ensure your custom classes are instantiated rather than the built in ones.
If you are OK with having the custom profile fields stored and accessed separately from the ZfcUser user entity, check out my module on GitHub: LdcUserProfile. It provides a profile system for ZfcUser but also makes it easy to add your own custom profile fieldsets linked to a user.

Related

How to add sortable column for member list in Silverstripe admin?

I am struggling to add sort functionality on one of my member summary fields in admin.
I have extended the Silverstripe member class using:
class MyMemberExtension extends DataExtension
I have added a few fields to the default gridfield in admin:
private static $db = array(
'Organisation' => 'Varchar(100)'
);
private static $summary_fields = array(
'FirstName' => 'First Name',
'Surname' => 'Surname',
'Email' => 'Email',
'OrganisationName' => 'Organisation Name',
'LastVisited' => 'Last Visited',
'NumVisit' => 'Num Visits'
);
private static $casting = array(
'OrganisationName' => 'Varchar(100)'
);
public function getOrganisationName() {
return $this->owner->Organisation;
}
...and that all works nicely.
However, only the core fields like LastVisited are giving me sort arrows on the column headers.
I'm currently stuck as to how to implement the sort on my Organisation field. I tried adding :
public function getCMSFields()
{
$fields = parent::getCMSFields();
$grid = $fields->dataFieldByName('Organisation');
$gridConfig = $grid->getConfig();
$gridConfig->addComponent(new GridFieldSortableHeader());
return $fields;
}
public function getEditForm($id = null, $fields = null) {
$form=parent::getEditForm($id, $fields);
$model = singleton($this->modelClass);
// add sorting if we have a field for...
if (class_exists('GridFieldSortableRows')
&& $model->hasField('Organisation')
&& $gridField=$form->Fields()->dataFieldByName($this->sanitiseClassName($this->modelClass))) {
if($gridField instanceof GridField) {
$gridField->getConfig()->addComponent(new GridFieldSortableRows('Organisation'));
}
}
return $form;
}
...to my class, but I'm not convinced these are even being called, as even if I just return null from these two functions nothing changes.
I have found a few answers that deal with extensions to ModelAdmin, but not for the core Member list. Thanks!
First of all, I'm not sure why you chose to have a getter named OrganisationName, where you could just as well use Organisation directly? That being said, I think your question is valid and might apply to different scenarios and/or field-types.
The Form-field that is being used to edit members is the Members GridField within SecurityAdmin. Luckily, there's an extension hook (updateEditForm) to modify the form fields of SecurityAdmin.
So in order to modify the sorting of the Members GridField, create an Extension like the following:
<?php
class MemberAdminExtension extends Extension
{
public function updateEditForm(Form $form)
{
/** #var GridField $memberGridField */
if ($memberGridField = $form->Fields()->dataFieldByName('Members')) {
/** #var GridFieldSortableHeader $sortHeader */
if ($sortHeader = $memberGridField->getConfig()->getComponentByType('GridFieldSortableHeader')) {
// Map OrganisationName to the Organisation field
$sortHeader->setFieldSorting([
'OrganisationName' => 'Organisation'
]);
}
}
}
}
And apply the extension via config to SecurityAdmin:
# Within _config/config.yml
SecurityAdmin:
extensions:
- MemberAdminExtension
After a dev/build your Member table should be sortable by Organisation Name as well…

How to make a custom action inaccessible depending on the user - Sonata Admin

I have implemented the clone action just like in the documentation. How can I limit the access to the clone action to the user who created the object?
I have already an access denied exception check in my action, but how can I now hide the button in the list view if the user is not the author of that object. The user should still be able to list the order and display it.
This is my route:
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('clone', $this->getRouterIdParameter().'/clone');
}
And my list fields:
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('_action', 'actions', array(
'actions' => array(
'show' => array(),
'edit' => array(),
'clone' => array(
'template' => 'AppBundle:Sonata/Button:clone_button.html.twig'
),
), 'label' => 'Actions'
))
;
}
and my clone action:
public function cloneAction($id = null)
{
$object = $this->admin->getSubject();
if (!$object) {
throw new NotFoundHttpException(sprintf('Unable to find the object with id : %s', $id));
}
If (!$object->isAuthor($this->getUser())) {
throw new AccessDeniedException();
}
$clonedObject = clone $object;
$this->admin->create($clonedObject);
$this->addFlash('sonata_flash_success', 'Cloned successfully');
return new RedirectResponse($this->admin->generateUrl('edit', array('id' => $clonedObject->getId())));
}
As you can see in my clone action, I have a check to see if the user is the author of the order. But how can I remove the button in the list completely by check my isAuthor function?
Because now the user can see the button but if he is unauthorized to clone the order and he clicks the button he get an access denied exception. So I don't want to show the button at all. The same counts for the edit button.
I have thought of something like this:
protected function configureRoutes(RouteCollection $collection)
{
$user = $this->getConfigurationPool()->getContainer()->get('security.token_storage')
->getToken()->getUser();
If (!$object->isAuthor($user)) {
$collection->remove('edit');
$collection->remove('clone');
}
}
But apparently this can't be done.
Does anybody have an idea how to do this?
I would create a Symfony Voter and remove the check from the action. The check would be done, in the voter outside the action, and could be done from anywhere, including the template. You should check the template, it probably already does the check.
Also, off-topic pro-tip, always provide a message inside your exceptions.
throw new AccessDeniedException('Not an author of this object');

Managed entity cached in same request and not updating

My code is fairly complex so I will try to explain in the simplest way possible
I have a parent entity ValueList. This 'list' has many ValueListItems.
class ValueList
{
//...
/**
* #ODM\ReferenceMany(
* targetDocument="JobboardBase\Entity\ValueListItem",
* sort={"order"="asc"},
* cascade={"all"}
* )
*/
protected $items;
}
I then have a service method that adds a new ValueListItem to this (already managed) ValueList.
public function createValueListItem(ValueListItem $item, ValueList $list)
{
try {
$om = $this->getObjectManager();
$om->persist($item);
$list->addItem($item);
$om->persist($list);
$om->flush();
return $item;
} catch (\Exception $e) {
throw $e;
}
}
This adds the entity correctly to the Mongo collection. However because I am executing the controller action with an AJAX call I also need to re-dispatch the 'indexAction' to return a the updated 'list' HTML asynchronously.
// ListItemController::createAndAttachValueItemToParentListAction()
// ....
// Below is the successful 'add' of the above method call return
if ($service->createValueListItem($form->getData(), $list)) {
$content = $this->forward()->dispatch('JobboardBase\Controller\ListItem', array(
'action' => 'index',
'id' => $list->getId()
));
return $this->jsonModel(array(
'success' => true,
'messages' => array($message),
'content' => $content
));
//... IndexAction
public function indexAction() {
// ...
$items = $list->getItems(); // Returns 0 (when there should be 1)
//...
}
The HTML returned via the forward() call (in $content) doesn't include the new added ValueListItem entity. It will however display correctly when I refresh the page.
Doctrine seems to be returning a cached ValueList entity that doesn't include the newly added ValueListItem - Only when a new requested is made does the new item get displayed.
My question is why is doctrine returning the 'old' entity rather than the updated entity? I was under the impression that it should be the same instance and therefore updated by reference?
you can refresh your model with the actual data using entity manager refresh method:
$om->refresh($list);

Can FormBuilder be used in a Service class to build forms not connected to an Entity class?

I would like to use Symfony2's FormBuilder Component to build dynamic forms within a service class. However, these forms will not be linked to any entities immediately (so I don't think the Form Class service type would apply.)
I need to ask our users a series of questions with a myriad of outcomes depending on how the questions are answered.(For example: Do you like cheesecake? If yes, the user will be directed to another form asking what kind of cheescake; if no, ask if you like ice cream.. etc.) To do this I created a Dialoguer service which is instantiated with the FormFactory service. From services.yml:
dialoguer:
class: My\MainBundle\Services\Dialoguer
arguments: ['#forms.factory']
Each step of the dialog process is handled by a different dialogue class - all of which are children of an AbstractDialogue class. The dialoguer service looks at data that has already been answered and finds the appropriate dialog to ask next.
The dialoguer service passes the #forms.factory service to the appropriate dialog class, in this case Start:
namespace My\MainBundle\Services\Dialoguer\Individual;
use My\MainBundle\Services\Dialoguer\AbstractDialogue as AbstractDialogue;
class Start extends AbstractDialogue
{
static $stem = 'check_registration';
protected $fields = array(
'over_thirtyfive' => array(
'type' => 'choice',
'options' => array(
'label' => 'Are you over 35?',
'choices'=>array('yes'=>'Yes','no'=>'No'),
'expanded'=> true,
'multiple'=>false
)
)
);
....
function __construct( $formFactory )
{
$this->formFactory = $formFactory;
}
From the parent, AbstractDialogue class, the get_form method goes through each field and adds it the form based on the specifications above.
public function get_form( $form_data = array() )
{
$defaults = array_merge( $this->defaults, $form_data );
$builder = $this->formFactory->create();
$builder->setData( $defaults );
foreach( $this->fields as $field => $type_ar )
{
$builder = $builder->add( $field, $type_ar['type'], $type_ar['options'] );
}
//return $builder->getForm();
return $builder;
}
And, finally in the controller class:
$dialogue = $this->get('dialoguer')->process($request, 'Individual');
$form = $dialogue->get_form()->getView();
$args = compact('form');
return $this->render("MyMainBundle:Forms:process_app.form.html.twig", $args);
Finally, here's the issue:
Above, my get_form method passes a Symfony\Component\Form\Form instance, which doesn't have the getView method - so it throws an error.
What do I need to do get a form view using this non-traditional method of creating a form?
(Or is this not going to work?)
I had a typo in my Controller that prevented the form from accessing the View. 'getView' should be changed to 'createView'. Whoops.
I also made a change to my service, changing '$this->formFactory->create()' to '$this->formFactory->createBuilder();'
For folks in the future, I'll walk through the script in the service class and the accompanying apis:
public function get_form( $form_data = array() )
{
$defaults = array_merge( $this->defaults, $form_data );
$builder = $this->formFactory->createBuilder();
As I mentioned in the original post - $this->formFactory is made available to my class by importing '#form.factory' through my service configuration. That's createBuilder instantiates an instance of Symfony\Component\Form\FormBuilder ~ http://api.symfony.com/2.0/Symfony/Component/Form/FormBuilder.html
$builder->setData( $defaults );
foreach( $this->fields as $field => $type_ar )
{
$builder = $builder->add( $field, $type_ar['type'], $type_ar['options'] );
}
return $builder->getForm();
}
The getForm method instantiates an instance of Symfony\Component\Form\Form ~ http://api.symfony.com/2.0/Symfony/Component/Form/Form.html
And then in my Controller -
$form = $dialogue->get_form()->createView();
The createView method creates an instance of Symfony\Component\Form\FormView which can the be rendered in Twig:
{{ form(form) }}
It works.

Zend Framework 2 - Hydrator strategy for Doctrine relationship not working

As mentioned here I'm building a custom hydration strategy to handle my related objects in a select box in a form.
My form looks like this:
$builder = new AnnotationBuilder($entityManager);
$form = $builder->createForm(new MyEntity());
$form->add(new MyFieldSet());
$hydrator = new ClassMethodsHydrator();
$hydrator->addStrategy('my_attribute', new MyHydrationStrategy());
$form->setHydrator($hydrator);
$form->get('my_attribute')->setValueOptions(
$entityManager->getRepository('SecEntity\Entity\SecEntity')->fetchAllAsArray()
);
When I add a new MyEntity via the addAction everything works great.
I wrote fetchAllAsArray() to populate my selectbox. It lives within my SecEntityRepository:
public function fetchAllAsArray() {
$objects = $this->createQueryBuilder('s')
->add('select', 's.id, s.name')
->add('orderBy', 's.name ASC')
->getQuery()
->getResult();
$list = array();
foreach($objects as $obj) {
$list[$obj['id']] = $obj['name'];
}
return $list;
}
But in the edit-case the extract() function doesn't work. I'm not at the point where I see something of hydrate() so I'll leave it out for now.
My hydrator strategy looks like this:
class MyHydrationStrategy extends DefaultStrategy
{
public function extract($value) {
print_r($value);
$result = array();
foreach ($value as $instance) {
print_r($instance);
$result[] = $instance->getId();
}
return $result;
}
public function hydrate($value) {
...
}
The problem is as follows:
Fatal error: Call to a member function getId() on a non-object
The print_r($value) returns loads of stuff beginning with
DoctrineORMModule\Proxy__CG__\SecEntity\Entity\SecEntity Object
following with something about BasicEntityPersister and somewhere in the mess are my referenced entities.
The print_r($instance) prints nothing. It's just empty. Therefore I guess is the error message legit... but why can't I iterate over these objects?
Any ideas?
Edit:
Regarding to #Sam:
My attribute in the entity:
/**
* #ORM\ManyToOne(targetEntity="Path/To/Entity", inversedBy="whatever")
* #ORM\JoinColumn(name="attribute_id", referencedColumnName="id")
* #Form\Attributes({"type":"hidden"})
*
*/
protected $attribute;
My new selectbox:
$form->add(array(
'name' => 'attribute',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'MyLabel',
'object_manager' => $entityManager,
'target_class' => 'Path/To/Entity',
'property' => 'name'
)
));
My final hope is that I'm doing something wrong within the controller. Neither my selectbox is preselected nor the value is saved...
...
$obj= $this->getEntityManager()->find('Path/To/Entity', $id);
$builder = new \MyEnity\MyFormBuilder();
$form = $builder->newForm($this->getEntityManager());
$form->setBindOnValidate(false);
$form->bind($obj);
$form->setData($obj->getArrayCopy());
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$form->bindValues();
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('entity');
}
}
I still haven't come around to write the tutorial for that :S
I don't know if this is working with the annotationbuilder though! As the DoctrineModule\Form\Element\ObjectSelect needs the EntityManager to work. The options for the ObjectSelect are as follows:
$this->add(array(
'name' => 'formElementName',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'formElementLabel',
'empty_option' => '--- choose formElementName ---',
'object_manager' => $this->getEntityManager(),
'target_class' => 'Mynamespace\Entity\Entityname',
'property' => 'nameOfEntityPropertyAsSelect'
)
));
In this case i make use of $this->getEntityManager(). I set up this dependency when calling the form from the ServiceManager. Personally i always do this from FactoryClasses. My FormFactory looks like this:
public function createService(ServiceLocatorInterface $serviceLocator)
{
$em = $serviceLocator->get('Doctrine\ORM\EntityManager');
$form = new ErgebnishaushaltProduktForm('ergebnisform', array(
'entity_manager' => $em
));
$classMethodsHydrator = new ClassMethodsHydrator(false);
// Wir fügen zwei Strategien, um benutzerdefinierte Logik während Extrakt auszuführen
$classMethodsHydrator->addStrategy('produktBereich', new Strategy\ProduktbereichStrategy())
->addStrategy('produktGruppe', new Strategy\ProduktgruppeStrategy());
$hydrator = new DoctrineEntity($em, $classMethodsHydrator);
$form->setHydrator($hydrator)
->setObject(new ErgebnishaushaltProdukt())
->setInputFilter(new ErgebnishaushaltProduktFilter())
->setAttribute('method', 'post');
return $form;
}
And this is where all the magic is happening. Magic, that is also relevant to your other Thread here on SO. First, i grab the EntityManager. Then i create my form, and inject the dependency for the EntityManager. I do this using my own Form, you may write and use a Setter-Function to inject the EntityManager.
Next i create a ClassMethodsHydrator and add two HydrationStrategies to it. Personally i need to apply those strategies for each ObjectSelect-Element. You may not have to do this on your side. Try to see if it is working without it first!
After that, i create the DoctrineEntity-Hydrator, inject the EntityManager as well as my custom ClassMethodsHydrator. This way the Strategies will be added easily.
The rest should be quite self-explanatory (despite the german classnames :D)
Why the need for strategies
Imo, this is something missing from the DoctrineEntity currently, but things are still in an early stage. And once DoctrineModule-Issue#106 will be live, things will change again, probably making it more comfortable.
A Strategy looks like this:
<?php
namespace Haushaltportal\Stdlib\Hydrator\Strategy;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class ProduktbereichStrategy implements StrategyInterface
{
public function extract($value)
{
if (is_numeric($value) || $value === null) {
return $value;
}
return $value->getId();
}
public function hydrate($value)
{
return $value;
}
}
So whenever the $value is not numeric or null, meaning: it should be an Object, we will call the getId() function. Personally i think it's a good idea to give each Element it's own strategy, but if you are sure you won't be needing to change the strategy at a later point, you could create a global Strategy for several elements like DefaultGetIdStrategy or something.
All this is basically the good work of Michael Gallego aka Bakura! In case you drop by the IRC, just hug him once ;)
Edit An additional resource with a look into the future - updated hydrator-docs for a very likely, soon to be included, pull request

Categories