I have some entity, but i want to validate a maximum of X entries in db.
For example, no more than 5 categories in DB are allowed.
I try to see if some constrain validation solve my trouble, but i dont find nothing useful.
Thank in advance
If you need to validate your entity in multiple places or do this type of validation for more than one entity class you could write a custom validation constraint to do this. It does feel a bit like overkill but it is technically the 'correct' solution. This is documented in the Symfony Manual in How to create a Custom Validation Constraint. For example:
Constraint class:
<?php
namespace Your\Bundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class MaxEntries extends Constraint
{
public $message = 'The maximum %max% %name% entries already exist.';
public $max = 5;
public function validatedBy()
{
return 'maxEntries';
}
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
ConstraintValidator class:
<?php
namespace Your\Bundle\Validator\Constraints;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class MaxEntriesValidator extends ConstraintValidator
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function validate($protocol, Constraint $constraint)
{
/** #var MaxEntries $constraint */
$protocolClass = new \ReflectionClass(get_class($protocol));
$entityName = $protocolClass->getShortName();
// alternative to next 2 lines - you could use dql to do a select count
$repository = $this->entityManager->getRepository('YourBundle:' . $entityName);
$entities = $repository->findAll();
if (count($entities) == $constraint->max)
{
$this->context->addViolation($constraint->message, array(
'%max%' => $constraint->max,
'%name%' => $entityName,
));
}
}
}
services.yml:
validator.max_entries:
class: Bullitt\TargetNeutral\SpectatorBundle\Validator\Constraints\MaxEntriesValidator
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: validator.constraint_validator, alias: maxEntries }
validation.yml:
Your\Bundle\YourEntity:
constraints:
- Your\Bundle\Validator\Constraints\MaxEntries:
# You can override the default constraint attributes here
message: "Failed! The maximum %max% %name% entries already exist."
max: 42
Note, you might be able to come up with a better name than MaxEntries (I normally spend more time thinking of the most meaningful name). Also, the validator class currently contains no protection for applying it to a class that is not a doctrine entity.
You will probably have to implement this yourself. I haven't seen any such thing, but it wouldn't be difficult to do one your own. Just add a function to check how many entries you have in the db, and call this function before you insert anything.
Something like this can be done:
public function newAction(){
$isAllowed = $this->checkMaxRecords();
if($isAllowed){
//save to the database
}
}
private function checkMaxRecords(){
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
'SELECT c
FROM AcmeStoreBundle:Category c'
);
$category = $query->getResult();
if(count($category) >= 5){
return false;
}
return true;
}
This is untested code, but something along these lines should get you on the right track.
Related
I need an advice for my Symfony 3.4 project (yes I know there is Symfony 6), I have an entity Product and I created a method that calculates the completion percentage of a Product sheet. It checks many properties of the entity and some other entities related to it (collections).
For now, I placed that method inside my Product entity and it works well. But for a specific thing, I need to do a complex query to the database and I can't use the query builder in the Entity class. So I'm wondering if I should place that code in the ProductController or maybe in the ProductRepository ?
Is it possible to use the entity object in the repository ? I don't need to build queries for each check, I simply use entity getters for the most of the checks.
Then, I will show the result in several pages of my project.
My function is someting like this (simplified) :
public function checkSetup()
{
$setup = array(
'active' => $this->isActive(),
'ref' => !empty($this->ref) ? true : false,
'tags' => $this->tags->isEmpty() ? false : true,
);
// I want to add the following part :
$qb = $em->getRepository(Product::class)->createQueryBuilder('p');
// build complex query...
$records = $qb->getQuery()->getResult();
$setup['records'] = !empty($records) ? false : true;
// Completion level
$score = 0;
foreach ($setup as $s) {
if ($s) $score++;
}
$num = $score / count($setup) * 100;
$setup['completion'] = round($num);
return $setup;
}
Based on the comment of #Cerad :
Create a specific class for the checker.
<?php
// src/Checker/ProductChecker.php
namespace AppBundle\Checker;
use AppBundle\Entity\Product\Product;
use Doctrine\ORM\EntityManager;
class ProductChecker
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function check(Product $p)
{
// code...
$result = $this->em->getRepository(Product::class)->customQuery($p->getId());
// code...
}
}
Register ProductChecker as a service and inject EntityManager for using ProductRepository in ProductChecker.
# app/config/services.yml
app.product_checker:
public: true
class: AppBundle\Checker\ProductChecker
arguments: ["#doctrine.orm.entity_manager"]
Write the complex query in ProductRepository.
Then, call the checker in the Controller :
$checker = $this->get("app.product_checker");
$report = $checker->check($product);
I have this entity:
AppBundle\Entity\Ciudad
class Ciudad{
...
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ComunidadAutonoma")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_ccaa", referencedColumnName="id")
* })
*/
private $ccaa;
....
public function getCcaa()
{
return $this->ccaa;
}
public function setCcaa(ComunidadAutonoma $ccaa)
{
$this->ccaa = $ccaa;
}
}
And the other entity is:
AppBundle\Entity\ComunidadAutonoma
class ComunidadAutonoma{
properties
getters
setters
}
In a controller, I get data from a form, and I´m triying to deserialize the data into a Ciudad entity, but is getting me allways the same error:
Expected argument of type "AppBundle\Entity\ComunidadAutonoma", "integer" given
In the form data I send to the action in the controller, the value of the comunidadautonoma is the id of the selected option in a combo:
{
parameters...
ccaa:7,
parameters...
}
In my controller I have this:
<?php
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use AppBundle\Entity\Ciudad;
class CiudadController extends Controller
{
public function procesarAction(Request $request)
{
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$this->serializer = new Serializer($normalizers, $encoders);
$ciudad= $this->serializer->deserialize($parametros['parametros'], Ciudad::class, 'json');
}
}
Am I missing something?Do I need any special configuration to deserializer an entity with a relation?
You dont have to do anything if you properly configured a type. While creating a Form Type for your entity please add class name to your type like:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Ciudad::class,
]);
}
And please use english naming for your projects.
First of all, since you are sending form data to your controller you could use Form Type classes to leverage all the power of the Symfony Form Component that will all this job for you.
Answering your specific question (and assuming you cannot/don't want to use Symfony Form Component) this error is absolutely expected. As you can see in your setCcaa function declaration inside Ciudad class:
public function setCcaa(ComunidadAutonoma $ccaa)
Because of the type-hinting (ComunidadAutonoma $ccaa) setCcaa function expects an argument of type ComunidadAutonoma. Now when Symfony serializer tries to denormalize your json object it calls setCcaa function with argument the ccaa value provided in your json (in your example is 7) which happens to be an integer. So Symfony complains that you provide an integer instead of ComunidadAutonoma type.
In order to solve this problem you have to create and use your own normalizer so that you can transform this integer to the corresponding entity object from your database. Something like this:
class EntityNormalizer extends ObjectNormalizer
{
/**
* Entity manager
* #var EntityManagerInterface
*/
protected $em;
public function __construct(
EntityManagerInterface $em,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null
) {
parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
// Entity manager
$this->em = $em;
}
public function supportsDenormalization($data, $type, $format = null)
{
return strpos($type, 'App\\Entity\\') === 0 && (is_numeric($data) || is_string($data));
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->em->find($class, $data);
}
}
What this normalizer does is that it checks if your data type (in this case $ccaa) is type of an entity and if the data value provided (in this case 7) is an integer, it transforms this integer to the corresponding entity object from your database (if existing).
To get this normalizer working you should also register it in your services.yaml configuration, with the appropriate tags like this:
services:
App\Normalizer\EntityNormalizer:
public: false
autowire: true
autoconfigure: true
tags:
- { name: serializer.normalizer }
You could also set the normalizer's priority but since the default priority value is equal to 0 when Symfony's built-in normalizers' priority is by default negative, your normalizer will be used first.
You could check a fully explained example of this in this fine article.
From the examples I'm finding in the Symfony docs, it looks like the typical thing to do when needing to save data is something like in the controller class:
public function createAction(){
$product = new Product();
$product->setName('Amy Keyboard');
$product->setPrice(24.99);
$product->setDescription('Ergonomic and stylish!');
$em = $this->getDoctrine()->getManager();
$em->persist($product);
$em->flush();
return $this->render('index.html.twig');
}
It would be really great to not have to type those 3 $em lines in every single controller method! And it would be even sweeter to move all of this logic to a class somewhere else and then just call $product->saveProduct($data)! What is the best option here?
I usually create a manager class e.g. ProductManager and register it as service. I inject the EntityManager via setter injection and implement all the methods I need.
In your case this would look similar to this:
AppBundle/Product/ProductManager
namespace AppBundle\Product;
use Doctrine\ORM\EntityManager;
class ProductManager {
/** #var EntityManager */
private $entityManager;
public function setEntityManager (EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function getAll()
{
return $this->entityManager->createQuery('SELECT p FROM '.Product::class.' p')
->getResult();
}
public function add(Product $product, $flush = true)
{
$this->entityManager->persist($product);
if ( $flush ) {
$this->entityManager->flush($product);
}
}
public function byId($id)
{
// Fetch a product by id (note: No need to use DQL or the EntityRepository here either!)
return $this->entityManager->find(Product::class, $id);
}
}
app/config/services.yml
services:
app.product_manager:
class: AppBundle\Product\ProductManager
calls:
- [setEntityManager, ['#doctrine.orm.entity_manager']]
Controller
public function createAction(){
$product = new Product();
$product->setName('Amy Keyboard');
$product->setPrice(24.99);
$product->setDescription('Ergonomic and stylish!');
// add the product
$this->get('app.product_manager')->add($product);
return $this->render('index.html.twig');
}
Take a look at the Propel project if you want something like $product->save() but it is a totally different approach. This is the official bundle https://github.com/propelorm/PropelBundle/blob/3.0/README.markdown
So I have a couple doctrine entities, a Subscription and a Subscriber. There are many Subscriptions to a single subscriber (manyToOne). I wrote custom normalizers for both entities, but am having trouble getting the Subscriber to show up in the Subscription once it has been normalized to JSON.
The only way I've been able to get it to work is by passing the 'Subscriber' normalizer to the 'Subscription' normailizer. It seems like I should just be able to use the SerializerAwareNormalizer Trait, or something like that, to have Symfony recursively normalize my related entities.
services:
acme.marketing.api.normalizer.subscription:
class: acme\MarketingBundle\Normalizer\SubscriptionNormalizer
arguments: ['#acme.marketing.api.normalizer.subscriber']
public: false
tags:
- { name: serializer.normalizer }
acme.marketing.api.normalizer.subscriber:
class: acme\MarketingBundle\Normalizer\SubscriberNormalizer
public: false
tags:
- { name: serializer.normalizer }
and the normalizer...
<?php
namespace acme\MarketingBundle\Normalizer;
use acme\MarketingBundle\Entity\Subscription;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class SubscriptionNormalizer implements NormalizerInterface
{
private $subscriberNormalizer;
public function __construct($subscriberNormalizer)
{
$this->subscriberNormalizer = $subscriberNormalizer;
}
public function normalize($subscription, $format = null, array $context = [])
{
/* #var $subscription Subscription */
$subscriber = $subscription->getSubscriber();
return [
"id" => $subscription->getId(),
"subscriber" => $this->subscriberNormalizer->normalize($subscriber, $format)
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Subscription;
}
}
Is there a better way to accomplish this?
Spent a few hours on google and couldn't figure it out. Post on SO and 5 minutes later hit the right google link :(. Answer seems to be to implement NormalizerAwareInterface on the custom normalizer, and then use the NormalizerAwareTrait to get access to the normalizer for nested entities.
<?php
namespace acme\MarketingBundle\Normalizer;
use acme\MarketingBundle\Entity\Subscription;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class SubscriptionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function normalize($subscription, $format = null, array $context = [])
{
return [
"id" => $subscription->getId(),
"subscriber" => $this->normalizer->normalize($subscription->getSubscriber())
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Subscription;
}
}
In a Symfony2 project, I have a Doctrine entity that has a datetime field, called lastAccessed. Also, the entity uses Timestampable on updatedAt field.
namespace AppBundle\Entity;
use
Doctrine\ORM\Mapping as ORM,
Gedmo\Mapping\Annotation as Gedmo
;
class MyEntity {
/**
* #ORM\Column(type="datetime")
*/
private $lastAccessed;
/**
* #ORM\Column(type="datetime")
* #Gedmo\Timestampable(on="update")
*/
private $updatedAt;
}
I need to update the field lastAccessed without also updating the updatedAt field. How can I do that?
I just stumbled upon this as well and came up with this solution:
public function disableTimestampable()
{
$eventManager = $this->getEntityManager()->getEventManager();
foreach ($eventManager->getListeners('onFlush') as $listener) {
if ($listener instanceof \Gedmo\Timestampable\TimestampableListener) {
$eventManager->removeEventSubscriber($listener);
break;
}
}
}
Something very similar can be used to disable the blamable behavior as well of course.
There is also an easy way (or more proper way) how to do it, just override listener.
first create interface which will be implemented by entity
interface TimestampableCancelInterface
{
public function isTimestampableCanceled(): bool;
}
than extend Timestampable listener and override updateField.
this way we can disable all all events or with cancelTimestampable define custom rules for cancellation based on entity state.
class TimestampableListener extends \Gedmo\Timestampable\TimestampableListener
{
protected function updateField($object, $eventAdapter, $meta, $field)
{
/** #var \Doctrine\Orm\Mapping\ClassMetadata $meta */
$property = $meta->getReflectionProperty($field);
$newValue = $this->getFieldValue($meta, $field, $eventAdapter);
if (!$this->isTimestampableCanceled($object)) {
$property->setValue($object, $newValue);
}
}
private function isTimestampableCanceled($object): bool
{
if(!$object instanceof TimestampableCancelInterface){
return false;
}
return $object->isTimestampableCanceled();
}
}
implement interface. Most simple way is to just set property for this
private $isTimestampableCanceled = false;
public function cancelTimestampable(bool $cancel = true): void
{
$this->isTimestampableCanceled = $cancel;
}
public function isTimestampableCanceled():bool {
return $this->isTimestampableCanceled;
}
or define rules like you want
last thing is to not set default listener but ours.
I'm using symfony so:
stof_doctrine_extensions:
orm:
default:
timestampable: true
class:
timestampable: <Namespace>\TimestampableListener
Than you can just do
$entity = new Entity;
$entity->cancelTimestampable(true)
$em->persist($entity);
$em->flush(); // and you will get constraint violation since createdAt is not null :D
This way can be timestamps disabled per single entity not for whole onFlush. Also custom behavior is easy to apply based on entity state.
Timestampable is just doctrine behavior so is executed every time when you use an ORM.
In my opinion the simplest way is just using DBAL layer and raw sql query.
For example:
$sql = "UPDATE my_table set last_accessed = :lastAccess where id = :id";
//set parameters
$params['lastAccess'] = new \DateTime();
$params['id'] = $some_id;
$stmt = $this->entityManager->getConnection()->prepare($sql);
$stmt->execute($params);
You can of course put it into proper repository class