I am building an app based on Sylius standard edition. With the ResourceBundle i handled to integrate my own entities and the corresponding relations. This new resources should be related later to the products entity, but first i want to get it working "standalone". The backend works for both, the added resource and for relations. These are editable via form-collections. Very fine! Now i want to get translated database-content for my new resource. I tried the way i did it in Symfony earlier, but it didn't work. The last vew days i tried every possible solution found, but none of this works, or i made mistakes... Neither the translation-tables where constructed when typing:
app/console doctrine:schema:update --force
nor translatable content is visible in the forms. When calling the edit action, i get following error:
error:
Neither the property "translations" nor one of the methods "getTranslations()", "translations()", "isTranslations()", "hasTranslations()", "__get()" exist and have public access in class "KontaktBundle\Entity\Kontakte".
Is somebody out there with an example implementation of a extended Sylius-resource with translateable database-content? I'm still learning symfony and sylius too, can you tell me what i'm missing or doing wrong?
Many Thanks to #gvf. Now i figured out that the config entry must be set. This gave me a functional example which i want to provide here:
Configs
# app/config/sylius_config.yml (must be imported in config)
# Adding Resource
sylius_resource:
# Resource Settings
settings:
sortable: true
paginate: 50
allowed_paginate: [50, 100, 500]
filterable: true
resources:
dbk.authors:
driver: doctrine/orm
templates: AuthorBundle:Backend
object_manager: default
classes:
model: AuthorBundle\Entity\Kontakte
#interface: // if you have an interface configured
controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
repository: Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository
translation:
model: AuthorBundle\Entity\AuthorsTranslation
mapping:
fields: ['anrede','biografie']
Services
# app/config/sylius_services.yml (must be imported in config)
parameters:
# Parameter for our author entity
app.form.type.authors.class: AuthorBundle\Form\AuthorsType
app.form.type.authors_translation.class: AuthorBundle\Form\AuthorsTranslationType
services:
# Adding Authors Backend menu Item
dbk_backend_authors.menu_builder:
class: AuthorBundle\EventListener\MenuBuilderListener
tags:
- { name: kernel.event_listener, event: sylius.menu_builder.backend.main, method: addBackendMenuItems }
- { name: kernel.event_listener, event: sylius.menu_builder.backend.sidebar, method: addBackendMenuItems }
# Adding Authors FormType
app.form.type.authors:
class: "%app.form.type.authors.class%"
tags:
- {name: form.type, alias: dbk_authors }
app.form.type.authors_translation:
class: "%app.form.type.authors_translation.class%"
tags:
- {name: form.type, alias: dbk_authors_translation }
EventListener (Adds Menu Entry in Sylius-Backend)
<?php
// AuthorBundle/EventListener/MenuBuilderListener/MenuBuilderListener.php
namespace AuthorBundle\EventListener;
use Sylius\Bundle\WebBundle\Event\MenuBuilderEvent;
class MenuBuilderListener
{
public function addBackendMenuItems(MenuBuilderEvent $event)
{
$menu = $event->getMenu();
$menu['assortment']->addChild('vendor', array(
'route' => 'Authors',
'labelAttributes' => array('icon' => 'glyphicon glyphicon-user'),
))->setLabel('Authors');
}
}
Entities
Authors (The new resource hich we want to add and to translate)
<?php
// AuthorBundle/Entity/Authors.php
namespace AuthorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Translation\Model\AbstractTranslatable;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Authors
*
* #ORM\Entity
* #ORM\Table(name="authors")
*/
class Authors extends AbstractTranslatable
{
//
// IDENTIFIER FIELDS
//
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", length=20, nullable=false, options={"unsigned":"true"})
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
//
// FIELDS
//
/**
* #var string
*
* #ORM\Column(name="vorname", type="string", length=255)
*/
private $vorname;
/**
* #var string
*
* #ORM\Column(name="nachname", type="string", length=255)
*/
private $nachname;
public function __construct() {
parent::__construct();
}
//
// TranslationFields - Getters and Setters
//
/**
* Get Anrede
* #return string
*/
public function getAnrede()
{
return $this->translate()->getAnrede();
}
/**
* Set Anrede
*
* #param string $anrede
*
* #return Authors
*/
public function setAnrede($anrede)
{
$this->translate()->setAnrede($anrede);
return $this;
}
/**
* Get Biografie
* #return string
*/
public function getBiografie()
{
return $this->translate()->getBiografie();
}
/**
* Set Biografie
*
* #param string $biografie
*
* #return Authors
*/
public function setBiografie($biografie)
{
$this->translate()->setBiografie($biografie);
return $this;
}
//
// Getters and Setters
//
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set vorname
*
* #param string $vorname
*
* #return Authors
*/
public function setVorname($vorname)
{
$this->vorname = $vorname;
return $this;
}
/**
* Get vorname
*
* #return string
*/
public function getVorname()
{
return $this->vorname;
}
/**
* Set nachname
*
* #param string $nachname
*
* #return Authors
*/
public function setNachname($nachname)
{
$this->nachname = $nachname;
return $this;
}
/**
* Get nachname
*
* #return string
*/
public function getNachname()
{
return $this->nachname;
}
public function __toString(){
return $this->getFullName();
}
}
AuthorsTranslation (This is the Entity for tanslation of Authors)
<?php
// AuthorBundle/Entity/AuthorsTranslation.php
namespace AuthorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Translation\Model\AbstractTranslation;
/**
* AuthorsTranslation
*
* #ORM\Entity
* #ORM\Table(name="authors_translation")
*/
class AuthorsTranslation extends AbstractTranslation
{
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", length=20, nullable=false, options={"unsigned":"true"})
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
//
// TRANSLATABLE - FIELDS
//
/**
* #var string
* #ORM\Column(name="anrede", type="string", length=255)
*/
private $anrede;
/**
* #var string
* #ORM\Column(name="biografie", type="text")
*/
private $biografie;
/**
* {#inheritdoc}
*/
public function getId()
{
return $this->id;
}
//
// GETTERS AND SETTERS
//
/**
* Set anrede
*
* #param string $anrede
* #return Authors
*/
public function setAnrede($anrede)
{
$this->anrede = $anrede;
return $this;
}
/**
* Get anrede
*
* #return string
*/
public function getAnrede()
{
return $this->anrede;
}
/**
* Set biografie
*
* #param string $biografie
*
* #return Authors
*/
public function setBiografie($biografie)
{
$this->biografie = $biografie;
return $this;
}
/**
* Get biografie
*
* #return string
*/
public function getBiografie()
{
return $this->biografie;
}
}
FormTypes
AuthorsType
<?php
// AuthorBundle/Form/AuthorsType.php
namespace AuthorBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ORM\QueryBuilder;
class AuthorsType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Add Translations to Form.
->add('translations', 'a2lix_translations', array(
'required' => false,
'fields' => array(
'anrede' => array(
'label' => 'Anrede',
),
'biografie' => array(
'label' => 'Biografie',
'attr' => array('data-edit' => 'wysiwyg', 'rows' => '15'),
'required' => false,
)
)
))
->add('vorname', null, array(
'label' => 'Vorname',
'required' => false,
))
->add('nachname', null, array(
'label' => 'Nachname',
'required' => false,
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false,
'data_class' => 'AuthorBundle\Entity\Authors'
));
}
/**
* #return string
*/
public function getName()
{
return 'dbk_authors';
}
}
AuthorsTranslationType
<?php
// AuthorBundle/Form/AuthorsTranslationType.php
namespace AuthorBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class AuthorsTranslationType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('anrede', 'text', array(
'label' => 'Anrede'
))
->add('biografie', 'textarea', array(
'label' => 'Biografie'
))
;
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'dbk_authors_translation';
}
}
Form Template
{# AuthorBundle/Resources/views/backend/edit||add.html.twig - as you like... the call of form.translation (line 5) is the point here #}
{% form_theme form 'SyliusWebBundle:Backend:forms.html.twig' %}
<div class="row">
<div class="col-md-12">
{{ form_row(form.translations, {'attr': {'class': 'input-lg'}}) }}
{{ form_row(form.vorname) }}
{{ form_row(form.nachname) }}
</div>
</div>
Have a look at the Product and ProductTranslation under the models folder in Sylius Components to see an example of how Sylius implements it.
Kontakte needs to extend AbstractTranslatable, you also need to create a class KontakteTranslation that extends AbstractTranslation. Under sylius_resource you also need to configure the translations:
sylius_resource:
resources:
dbk.contact:
driver: doctrine/orm
templates: KontaktBundle:Backend
object_manager: default
classes:
model: KontaktBundle\Entity\Kontakte
controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
repository: Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository
translation:
model: KontaktBundle\Entity\KontakteTranslation
mapping:
fields: {...fields that will be translated...}
Get rid of gedmo translatable extension because Sylius doesn't use it.
Related
I'm trying to create an todo-list element.
This is my list element class:
<?php
/**
* Created by PhpStorm.
* User: Alan
* Date: 03-Feb-17
* Time: 3:18 AM
*/
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="Todos")
*/
class Todos
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="integer", length=100)
*/
private $categoryId;
/**
* #ORM\Column(type="integer", length=100)
*/
private $userId;
/**
* #ORM\Column(type="datetime", length=100)
*/
private $init_date;
/**
* #ORM\Column(type="datetime", length=100)
*/
private $comp_date;
//SETTERS AND GETTERS....
}
For this I have generated a formType
<?php
namespace AppBundle\Form;
use ....
class TodosType extends AbstractType
{
private $user;
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', TextType::class, array(
'label' => 'Nazwa',
'attr' => array(
'class' => 'name-pick'
)
))
->add('categoryId', HiddenType::class)
->addEventListener(FormEvents::POST_SUBMIT, function(FromEvent $e){
$e->getData()->setUserId($this->user->getId());
$e->getData()->setInitDate($this->timestamp(new \DateTime()));
});
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Todos'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_todos';
}
public function __construct($storage)
{
$this->user = $storage->getToken()->getUser();
}
}
And a Service to get the users Id:
app.form.todos:
class: AppBundle\Form\TodosType
arguments: ["#security.token_storage"]
tags:
- { name: form.type, alias: app_user_todos }
Now in my Twig I have it listed like so in order to have the category id values assigned properly
{ form_start(form) }}
{{ form_widget(form.name) }}
{{ form_widget(form.categoryId, {'value': thisCat[0].id}) }}
{# TODO: FIND A BETTER WAY TO SEND THE categoryId#}
{{ form_end(form) }}
Which on submit is serializeArray()'d and send to my Ajax Controllers method which isn't supposed to do much other then to assign the values and insert them into the database:
/**
* #Route("/ajax/addTodo", name="AddTodoAjax")
*/
public function AddTodoAjax(Request $request)
{
$form = $this->createForm(TodosType::class);
$form->handleRequest($request);
if($form->isValid() && $form->isSubmitted()){
$todo = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($todo);
$em->flush();
return $this->json($todo->getId());
}
}
}
Now the problem is, running the system in this configuration produces an 500 (Internal Server Error) with error content being:
Type error: Argument 1 passed to
AppBundle\Form\TodosType::AppBundle\Form{closure}() must be an
instance of AppBundle\Form\FromEvent, instance of
Symfony\Component\Form\FormEvent given (500 Internal Server Error)
This is the first time I've had such an issue.
I tried looking for the question here but all the similar problems are nothing but...well similar.
Does anyone know how to fix this issue?
All help would be amazing.
In your form type, you have
->addEventListener(FormEvents::POST_SUBMIT, function(FromEvent $e){
That needs to be
->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $e){
In Sonata I have created several lists and all work fine. (Please not that that was a while ago, so I may have done something there which fixed the issue I will describe here...).
Now I have created a new listing of Playlist entities:
As you can see in the picture, both the "Id" column and the "Aangemaakt op" columns are sortable, however the "Playlist" column is not.
Both the "Aangemaakt op" and the "Playlist" fields are date-fields, but since the "Aangemaakt op" field is sortable I would say that has nothing to do with it.
I have been searching the Sonata documentation, Google and StackOverflow, but haven't found any clue concerning this issue. I did find thread about sorting a list based on an Entity field, but my field isn't an entity.
Relevant code:
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper) {
$listMapper
->add('id')
->add('playlist_date', 'date', array('label' => 'Playlist'))
->add('created', 'datetime', array('label' => 'Aangemaakt op'))
->add(
'_action', 'actions', array(
'actions' => array(
'delete' => array(),
)
)
);
}
Some StackOverflow threads and an answer below mention adding 'sortable' => true to the field that must be sortable.
Doing that indeed makes the column clickable to sort, but clicking it results in the following exception:
Catchable Fatal Error: Argument 1 passed to
Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery::entityJoin()
must be of the type array, null given, called in
/path/of/my/project/sonata-project/doctrine-orm-admin-bundle/Datagrid/ProxyQuery.php
on line 142 and defined.
According to other StackOverflow threads that is because a join must be created. However, the field is simply a field of the same Mysql record as the other fields. I did find a StackOverflow thread mentioning this as well and in which they joined the same record in order to make this work, but I didn't get that to work. Besides, I thank that shouldn't be the way to order the contents of a column.
Does anyone have a clue?
Update in reaction to Hibatallah Aouadni's answer
As Hibatallah suggests, I added the following to my PlaylistsAdmin:
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('id')
->add('playlist_date')
->add('created');
}
This resulted in the following error message:
Notice: Undefined index: playlist_date
So I inspected my Entity and I found that it has a UniqueConstraint:
uniqueConstraints={#ORM\UniqueConstraint(name="playlist_date", columns={"playlist_date"})}
It does not have an actual "index" defined, but ofcourse it is. However as a test I added the following:
, indexes={#ORM\Index(name="playlist_date", columns={"playlist_date"})}
This didn't give any different result.
So still no luck at all :(
** Entity and Entity admin **
Entity Admin:
<?php
namespace Company\AdminBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\AdminBundle\Route\RouteCollection;
class PlaylistsAdmin extends Admin
{
protected $baseRoutePattern = 'playlists';
protected $baseRouteName = 'playlists';
protected function configureRoutes(RouteCollection $collection) {
$collection->clearExcept(array('list', 'delete', 'show'));
}
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper) {
$listMapper
->add('id')
->add('playlist_date', 'date', array('label' => 'Playlist'))
->add('created', 'datetime', array('label' => 'Aangemaakt op'))
->add(
'_action', 'actions', array(
'actions' => array(
'show' => array(),
/*'edit' => array(),*/
'delete' => array(),
)
)
);
}
public function getBatchActions() {
return array();
}
}
Entity:
<?php
namespace Company\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Playlist
*
* #ORM\Table(name="playlist", uniqueConstraints={#ORM\UniqueConstraint(name="playlist_date", columns={"playlist_date"})})
* #ORM\Entity()
*/
class Playlist
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", options={"unsigned"=true})
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #var \DateTime
*
* #ORM\Column(name="playlist_date", type="date", nullable=true)
*/
protected $playlistDate;
/**
* #var \DateTime
*
* #ORM\Column(name="created", type="datetime", nullable=true)
*/
protected $created;
/**
* #var \Doctrine\Common\Collections\Collection
*/
protected $video;
/**
* Constructor
*/
public function __construct() {
$this->video = new \Doctrine\Common\Collections\ArrayCollection();
$this->setCreated(new \DateTime());
}
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set playlistDate
*
* #param \DateTime $playlistDate
* #return Playlist
*/
public function setPlaylistDate($playlistDate) {
$this->playlistDate = $playlistDate;
return $this;
}
/**
* Get playlistDate
*
* #return \DateTime
*/
public function getPlaylistDate() {
return $this->playlistDate;
}
/**
* Set created
*
* #param \DateTime $created
* #return Playlist
*/
public function setCreated($created) {
$this->created = $created;
return $this;
}
/**
* Get created
*
* #return \DateTime
*/
public function getCreated() {
return $this->created;
}
/**
* Add video
*
* #param \Company\AppBundle\Entity\Video $video
* #return Playlist
*/
public function addVideo(\Company\AppBundle\Entity\Video $video) {
$this->video[] = $video;
return $this;
}
/**
* Remove video
*
* #param \Company\AppBundle\Entity\Video $video
*/
public function removeVideo(\Company\AppBundle\Entity\Video $video) {
$this->video->removeElement($video);
}
/**
* Get video
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getVideo() {
return $this->video;
}
}
Finally I found it, it's so absurd, in the configureListFields method, you have to call the attribute with its name not the DataBase name, so:
change
->add('playlist_date', 'date', array('label' => 'Playlist'))
to
->add('playlistDate', 'date', array('label' => 'Playlist'))
:D I can't beleive we spend all this time for some absurd mistake ;)
all you need to do is to add the argument sortable to array and true as value:
->add('playlist_date', 'date', array(
'label' => 'Playlist',
'sortable' => true
))
try to add the playlist field in configureDatagridFilters in your entity admin:
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('id')
->add('playlist_date')
->add('created');
}
and it will work ;)
I have a lot of Categories in database.
Here is Category Entity
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="categories")
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Category")
*/
protected $rootCategory;
/**
* #ORM\Column(type="text")
*/
protected $name;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set rootCategory
*
* #param \AppBundle\Entity\Category $rootCategory
*
* #return Category
*/
public function setRootCategory(\AppBundle\Entity\Category $rootCategory = null)
{
$this->rootCategory = $rootCategory;
return $this;
}
/**
* Get rootCategory
*
* #return \AppBundle\Entity\Category
*/
public function getRootCategory()
{
return $this->rootCategory;
}
}
I want to get all categories in my edit form
EditFormType:
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use AppBundle\Controller\CategoryController;
class EditPhotoFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$categoryController = new CategoryController();
$builder->add('title', 'text');
$builder->add('description', 'textarea');
$builder->add('category', EntityType::class, array(
'class' => 'AppBundle:Category',
'choices' => $categoryController->getCategories(),
));
}
public function getName()
{
return 'app_photo_edit';
}
}
getCategories()
public function getCategories() {
$em = $this->getDoctrine()->getManager();
return $em->getRepository('AppBundle:Category')->findAll();
}
I am getting next error:
Error: Call to a member function has() on null
Thats because there is not Doctrine in controller object. Where should i get Doctrine and Repository in this case?
How should i do it correct way?
First, you should NEVER instantiate any Controller class yourself. Controller classes are used by Symfony's Kernel to handle a request, and they are loaded automatically with dependencies to do so.
Right here, you don't even need to require the EntityManager in your FormType, because EntityType has a built-in option query_builder to do what you need:
$builder->add('category', EntityType::class, array(
'class' => 'AppBundle:Category',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c');
},
);
This should do the trick. (check here for more details)
However, if one day you really need to import a dependancy inside your Form (whether it is EntityManager or another service), here's how you should do:
A. import the given dependency in your constructor:
private $dependency;
public function __construct(Dependency $dependency)
{
$this->$dependency = $dependency;
}
B. Declare your Form as a Service, with your dependency's id as argument:
<service id="app.form.type.edit_photo"
class="AppBundle\Form\Type\EditPhotoFormType">
<tag name="form.type" />
<argument type="service" id="app.dependencies.your_dependency" />
</service>
Then use $this->dependency in your Form wherever you need.
Hope this helps! :)
I've created a custom form validator that does not seem to be working. It gets called during form submission and returns false when it should fail validation. But it doesn't seem to tell the form that validation has failed.
Here is the form validator: (code box scrolls)
<?php
namespace Redacted\AppBundle\Validator\Constraints;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RequiredIfPlainPasswordSetValidator extends ConstraintValidator
{
/**
* RequestStack instance.
*
* #var Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* dependency injection.
*
* #param Symfony\Component\HttpFoundation\RequestStack $requestStack
*/
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
/**
* if the plain password has been set, the current password must not be
* empty.
*
* #param string $currentPassword
* #param Symfony\Component\Validator\Constraint $constraint
*
* #return bool
*/
public function validate($currentPassword, Constraint $constraint)
{
$plainPassword = $this->requestStack->getCurrentRequest()->request
->get('security_settings')['plainPassword']['first'];
if ($plainPassword && !$currentPassword) {
return false;
}
return true;
}
}
And here is the constraint that goes with it:
<?php
namespace Redacted\AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class RequiredIfPlainPasswordSet extends Constraint
{
/**
* error message
*
* #var string
*/
protected $message = 'Please enter your current password.';
/**
* the alias for the related validator in services.yml
*
* #return string
*/
public function validatedBy()
{
return 'required_if_plain_password_set';
}
}
and the relevant part of app/config/services.yml:
services:
redacted.appbundle.required_if_plain_password_set:
class: Redacted\AppBundle\Validator\Constraints\RequiredIfPlainPasswordSetValidator
arguments: ["#request_stack"]
tags:
- { name: validator.constraint_validator, alias: required_if_plain_password_set }
I've created a custom form type: (code box scrolls)
<?php
namespace Redacted\AppBundle\Form;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
class SecuritySettingsFormType extends AbstractType
{
/**
* the name of the form type
*
* #return string
*/
public function getName()
{
return 'security_settings';
}
/**
* add fields to form
*
* #param Symfony\Component\Form\FormBuilderInterface $formBuilder
* #param array $options
*/
public function buildForm(FormBuilderInterface $formBuilder, array $options)
{
$formBuilder
->add('email')
->add('currentPassword', 'password')
->add('plainPassword', 'repeated', [
'type' => 'password',
'invalid_message' => 'Passwords must match.',
]);
}
/**
* configureOptions.
*
* #param Symfony\Component\OptionsResolver\OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$options = [
'data_class' => 'AppBundle\Entity\User',
'validation_groups' => ['security_settings'],
];
$resolver->setDefaults($options);
}
}
which validates against the User entity due to the data_type. Here is the relevant part of the User entity:
<?php
namespace Redacted\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Redacted\AppBundle\Validator\Constraints\RequiredIfPlainPasswordSet;
class User implements UserInterface, \Serializable
{
// ...
/**
* #var string (default: null)
*
* #Assert\NotBlank(message="user.email_required", groups={"security_settings"})
* #Assert\Email(message="security_settings.valid_email" groups={"security_settings"})
* #ORM\Column(name="email", type="string", length=255, nullable=true, unique=true)
*/
private $email = null;
/**
* current password - used for validation only
*
* #RequiredIfPlainPasswordSet(
* message="security_settings.current_password_required",
* groups={"security_settings"}
* )
* #var string
*/
protected $currentPassword;
// ... setters and getters for above, etc.
}
And finally, here's the controller method I'm using to check. I'll leave out the view because it's probably irrelevant. (I'm defining my controller as a service so no redirectToRoute(), etc.)
// in the controller class...
/**
* Security settings page - email, password, etc.
*
* #Route("/security-settings", name="security_settings")
* #Template()
*
* #param Symfony\Component\HttpFoundation\Request $request
*
* #return array|Symfony\Component\HttpFoundation\RedirectResponse
*/
public function securitySettingsAction(Request $request)
{
$loggedInUser = $this->tokenStorage->getToken()->getUser();
$form = $this->formFactory
->create(new SecuritySettingsFormType(), $loggedInUser);
$form->handleRequest($request);
if ($form->isValid()) {
$user = $form->getData();
// persist the user
$this->saveUserSettings($user);
// set a success message
$this->setSecuritySettingsFlashSuccess($request);
// redirect back
$route = $this->router->generate('security_settings');
return new RedirectResponse($route);
}
return ['form' => $form->createView()];
}
The idea is that the current password should only be required if the new password is entered. Although that validate() function is getting called and returning false when it should, the form's isValid() is returning true and it's saving. If I add a #Assert\NotBlank(groups={"security_settings"}) assertion to the User::currentPassword field, it does fire and fail successfully, so it is looking for validation annotations on that field.
What am I missing?
The problem was that the validate() method of the ConstraintValidator should not just return true or false, it should build a violation like so:
/**
* if the plain password has been set, the current password must not be
* empty.
*
* #param string $currentPassword
* #param Symfony\Component\Validator\Constraint $constraint
*/
public function validate($currentPassword, Constraint $constraint)
{
$plainPassword = $this->requestStack->getCurrentRequest()->request
->get('security_settings')['plainPassword']['first'];
if ($plainPassword && !$currentPassword) {
$this->context->buildViolation($constraint->message)
->atPath('currentPassword')->addViolation();
}
}
Thanks to my coworker for catching it. All works as expected now.
I am working with Symfony 2.6 and trying to setup PayumBundle (paypal express checkout) and I am getting an error
InvalidConfigurationException in BaseNode.php line 313: Invalid configuration for path "payum.security.token_storage": The storage entry must be a valid model class. It is set Acme\featuresBundle\Entity\PaymentToken
I am following the steps mentioned in there documetation
This is how my config.yml looks like
doctrine:
orm:
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
default:
auto_mapping: true
mappings:
payum:
is_bundle: false
type: xml
dir: %kernel.root_dir%/../vendor/payum/core/Payum/Core/Bridge/Doctrine/Resources/mapping
prefix: Payum\Core\Model
payum:
security:
token_storage:
Acme\featuresBundle\Entity\PaymentToken: { doctrine: orm }
storages:
Acme\featuresBundle\Entity\PaymentDetails: { doctrine: orm }
contexts:
paypal:
paypal_express_checkout_nvp:
username: 'asdasd'
password: 'adsasd'
signature: 'asdasdasd'
sandbox: true
This is my Entity PaymentToken
namespace Acme\featuresBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Payum\Core\Model\Token;
/**
* #ORM\Table
* #ORM\Entity
*/
class PaymentToken extends Token
{
}
And this is Entity PaymentDetails
namespace Acme\featuresBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Payum\Core\Model\Order as BaseOrder;
/**
* #ORM\Table
* #ORM\Entity
*/
class PaymentDetails extends BaseOrder
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #var integer $id
*/
protected $id;
}
I have gone through alot of documentation online and other posts like this but I dont understand why I am getting this error.
The storage entry must be a valid model class. It is set Acme\featuresBundle\Entity\PaymentToken
I cant even get to the controller so something tells me it is the config.yml configuration of Payum that is not set correctly. I have gone through the documentation over and over and over and I cant seem to find what am I doing wrong.
I will really appreciate any help in getting pass this error.
I finally managed to get it done.
I needed 4 files
PaymentController
Orders (Entity)
PaymentToken (Entity)
Orders (Model)
This is my PaymentController looks like
<?php
namespace ClickTeck\featuresBundle\Controller;
use ClickTeck\featuresBundle\Entity\Orders;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Payum\Paypal\ExpressCheckout\Nvp\Api;
use Payum\Core\Registry\RegistryInterface;
use Payum\Core\Request\GetHumanStatus;
use Payum\Core\Security\GenericTokenFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Sensio\Bundle\FrameworkExtraBundle\Configuration as Extra;
class PaymentController extends Controller
{
public function preparePaypalExpressCheckoutPaymentAction(Request $request)
{
$paymentName = 'paypal';
$eBook = array(
'author' => 'Jules Verne',
'name' => 'The Mysterious Island',
'description' => 'The Mysterious Island is a novel by Jules Verne, published in 1874.',
'price' => 8.64,
'currency_symbol' => '$',
'currency' => 'USD',
'clientId' => '222',
'clientemail' => 'xyz#abc.com'
);
$storage = $this->get('payum')->getStorage('ClickTeck\featuresBundle\Entity\Orders');
/** #var $paymentDetails Orders */
$paymentDetails = $storage->create();
$paymentDetails->setNumber(uniqid());
$paymentDetails->setCurrencyCode($eBook['currency']);
$paymentDetails->setTotalAmount($eBook['price']);
$paymentDetails->setDescription($eBook['description']);
$paymentDetails->setClientId($eBook['clientId']);
$paymentDetails->setClientEmail($eBook['clientemail']);
$paymentDetails['PAYMENTREQUEST_0_CURRENCYCODE'] = $eBook['currency'];
$paymentDetails['PAYMENTREQUEST_0_AMT'] = $eBook['price'];
$paymentDetails['NOSHIPPING'] = Api::NOSHIPPING_NOT_DISPLAY_ADDRESS;
$paymentDetails['REQCONFIRMSHIPPING'] = Api::REQCONFIRMSHIPPING_NOT_REQUIRED;
$paymentDetails['L_PAYMENTREQUEST_0_ITEMCATEGORY0'] = Api::PAYMENTREQUEST_ITERMCATEGORY_DIGITAL;
$paymentDetails['L_PAYMENTREQUEST_0_AMT0'] = $eBook['price'];
$paymentDetails['L_PAYMENTREQUEST_0_NAME0'] = $eBook['author'].'. '.$eBook['name'];
$paymentDetails['L_PAYMENTREQUEST_0_DESC0'] = $eBook['description'];
$storage->update($paymentDetails);
$captureToken = $this->getTokenFactory()->createCaptureToken(
$paymentName,
$paymentDetails,
'payment_done'
);
$paymentDetails['INVNUM'] = $paymentDetails->getId();
$storage->update($paymentDetails);
return $this->redirect($captureToken->getTargetUrl());
}
public function doneAction(Request $request)
{
$token = $this->get('payum.security.http_request_verifier')->verify($request);
$payment = $this->get('payum')->getPayment($token->getPaymentName());
// you can invalidate the token. The url could not be requested any more.
// $this->get('payum.security.http_request_verifier')->invalidate($token);
// Once you have token you can get the model from the storage directly.
//$identity = $token->getDetails();
//$order = $payum->getStorage($identity->getClass())->find($identity);
// or Payum can fetch the model for you while executing a request (Preferred).
$payment->execute($status = new GetHumanStatus($token));
$order = $status->getFirstModel();
// you have order and payment status
// so you can do whatever you want for example you can just print status and payment details.
return new JsonResponse(array(
'status' => $status->getValue(),
'response' => array(
'order' => $order->getTotalAmount(),
'currency_code' => $order->getCurrencyCode(),
'details' => $order->getDetails(),
),
));
}
/**
* #return RegistryInterface
*/
protected function getPayum()
{
return $this->get('payum');
}
/**
* #return GenericTokenFactoryInterface
*/
protected function getTokenFactory()
{
return $this->get('payum.security.token_factory');
}
}
This is my Orders Entity
<?php
namespace ClickTeck\featuresBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use ClickTeck\featuresBundle\Model\Orders as BasePaymentDetails;
/**
* Orders
*/
class Orders extends BasePaymentDetails
{
/**
* #var integer
*/
protected $id;
private $number;
private $description;
private $client_email;
private $client_id;
private $total_amount;
private $currency_code;
protected $details;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set number
*
* #param integer $number
* #return Orders
*/
public function setNumber($number)
{
$this->number = $number;
return $this;
}
/**
* Get number
*
* #return integer
*/
public function getNumber()
{
return $this->number;
}
/**
* Set description
*
* #param string $description
* #return Orders
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set client_email
*
* #param string $clientEmail
* #return Orders
*/
public function setClientEmail($clientEmail)
{
$this->client_email = $clientEmail;
return $this;
}
/**
* Get client_email
*
* #return string
*/
public function getClientEmail()
{
return $this->client_email;
}
/**
* Set client_id
*
* #param string $clientId
* #return Orders
*/
public function setClientId($clientId)
{
$this->client_id = $clientId;
return $this;
}
/**
* Get client_id
*
* #return string
*/
public function getClientId()
{
return $this->client_id;
}
/**
* Set total_amount
*
* #param float $totalAmount
* #return Orders
*/
public function setTotalAmount($totalAmount)
{
$this->total_amount = $totalAmount;
return $this;
}
/**
* Get total_amount
*
* #return float
*/
public function getTotalAmount()
{
return $this->total_amount;
}
/**
* Set currency_code
*
* #param string $currencyCode
* #return Orders
*/
public function setCurrencyCode($currencyCode)
{
$this->currency_code = $currencyCode;
return $this;
}
/**
* Get currency_code
*
* #return string
*/
public function getCurrencyCode()
{
return $this->currency_code;
}
/**
* Set details
*
* #param string $details
* #return Orders
*/
public function setDetails($details)
{
$this->details = $details;
return $this;
}
/**
* Get details
*
* #return string
*/
public function getDetails()
{
return $this->details;
}
}
This is my PaymentToken Entity
<?php
namespace ClickTeck\featuresBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Payum\Core\Model\Token;
/**
* PaymentToken
*/
class PaymentToken extends Token
{
}
This is my Orders model
<?php
namespace ClickTeck\featuresBundle\Model;
use Payum\Core\Model\ArrayObject;
class Orders extends ArrayObject
{
protected $id;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
}
Now when I call the Action
preparePaypalExpressCheckoutPaymentAction via route
I get redirected to make the payment
I can see the response in doneAction
Very neat library. Took me a while to figure it out and I am glad it works now. I am sure i have alot more to learn about Payum and I hope someone can confirm if this is the right way :)