Say I have tables A, B in MySQL and Doctrine entity classes with the same names. Those entities are managed by Doctrine and are basically created according to Symfony/Doctrine docs.
Now I want to create entity C with columns: x, y. Whenever this entity is created or updated, I want to set the column values:
x: select count(*) from A where (some condition)
y: select sum(y) from B where (other condition)
pull some other data from A or B and store it as column value for C.
I want to do this in PHP and not use mysql triggers. I can't achieve from inside the Entity classes, because they don't have access to entity manager. I don't want to do this in the controller, as I want insert/update operations to be standardized, and I will need to do it from multiple controllers, and I generally don't think the controller is a good place for logic like this.
So I need some kind of class which manages entity C.
My question is: How do I call this manager class and where do I place it in Symfony? I am pretty sure this is a common need in Symfony (to access multiple entities while creating another entity), but I don't know how it is called and if there is a standard practice with them.
you can define service in app/config/services.yml and pass Entity manager as argument
services:
app.service.some_service:
class: AppBundle\Service\SomeService
arguments: ["#doctrine.orm.default_entity_manager"]
place your logic inside service
use Doctrine\ORM\EntityManagerInterface;
use AppBundle\Entity\SomeEntity;
class SomeService
{
/**
* #var EntityManagerInterface
*/
protected $entityManager;
public function __construct(EntityManagerInterface $entityManager) {
$this->entityManager = $entityManager;
}
public function getSomeEntity($id) {
$entity = $this->entityManager->getRepository(SomeEntity::class);
// do some work, return result..
}
}
call it from controller
$someService = $this->get('app.service.some_service');
$someService->getSomeEntity($id);
:)
I think you should create a Doctrine Event Subscriber as described in the documentation
I'll try to explain the basics.
1) Declare the service
services:
c_entity_counter_subscriber:
class: AppBundle\EventListener\CounterSubscriber
tags:
- { name: doctrine.event_subscriber, connection: default }
2) In the Subscriber count A and B properties
namespace AppBundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\A;
use AppBundle\Entity\B;
use AppBundle\Entity\C;
class CounterSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
);
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->count($args);
}
public function postPersist(LifecycleEventArgs $args)
{
$this->count($args);
}
public function count(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof C) {
return;
}
$entityManager = $args->getEntityManager();
// ... count/sum entities from A/B classes using $entityManager and update $entity
}
}
Related
Before i start, Note that I'm learning symfony so keep that in mind ! I just want to understand how it works.
Here's what i am trying to achieve :
I would like to make a working crud example of entities inheritance using doctrine. So this is how my example looks like :
Abstract Parent class : character
Child class 1 : Magician
Child class 2 : Warrior
Child class 3 : Archer
So after reading some documentation i decided to use the STI (Single Table Inheritance) of Doctrine.
Parent class :
/**
* Character
*
* #ORM\Table(name="character")
* #ORM\Entity(repositoryClass="AppBundle\Repository\CharacterRepository")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"magician_db" = "Magician", "warrior_db" = "Warrior", "archer_db" = "Archer"})
*/
abstract class Character{
protected id;
protected name;
public function getId();
public function getName();
public function setName();
}
Child Class 1 :
class Warrior extends Character{
protected armor;
public function battleShout();
}
Child Class 2:
class Magician extends Character{
protected silk;
public function spellAnnounce();
}
Child Class 3:
class Archer extends Character{
protected leather;
public function arrows();
}
I managed to create the table in my db, and i successfully loaded my fixtures for tests purposes. I also made my main view work (listing all characters).
My Problem :
Now i want to be able to create, edit & delete a specific character in the list with a single form. So for example i would have a 'type' select field where i can select 'warrior' , 'magician' or 'archer' and then i would be able to fill in the specific fields of the chosen entity. So let's say i choose 'warrior' in the form, then i would like to be able to set the armor property (along with the parents one of course) and persist it in the database.
I don't know how to do it since my parent class is abstract so i can't create a form based on that object.
Thx in advance for your help, i really need it !
PS: If there is a better solution / implementation don't hesitate !
The easiest way is to provide all fields and to remove them according to the 'type' value.
To do that you have to implement the logic on the client side (for displaying purpose) and server side (so that the removed fields cannot be changed in your entity).
On the client side :
Use javascript to hide the types which can't be set for each 'type' change (you can use JQuery and the .hide() function).
On the server side:
Add a PRE_BIND event to your form type, to remove the fields from the form :
http://symfony.com/doc/current/components/form/form_events.html#a-the-formevents-pre-submit-event
Your Form should look like :
// ...
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
$form = $formFactory->createBuilder()
->add('type', ChoiceType::class)
->add('armor')
->add('silk')
->add('leather')
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$submittedData = $event->getData();
$form = $event->getForm();
switch($submittedData['type'])
{
case 'warrior':
$form->remove('silk');
$form->remove('leather');
break;
case 'magician':
$form->remove('armor');
$form->remove('leather');
break;
case 'archer':
$form->remove('armor');
$form->remove('silk');
break;
default:
throw new ...;
}
})
->getForm();
// ...
EDIT
To deal with Single Table Inheritance, you can't use an abstract class, the base class must be a normal entity.
In your form, just set the class as AppBundle\Character.
In your controller action which creates the character, you must initiate your entity with something like this :
if($request->isMethod('POST')){
// form has been submitted
switch($request->get('type'))
{
case 'warrior':
$entity = new Warrior();
...
}
}
else{
// form has not been submitted, default : Warrior
$entity = new Warrior();
}
By editing and removing the character, you can directly deal with the Character Entity.
I recommand to not let the user change the type by edit, see Doctrine: Update discriminator for SINGLE_TABLE Inheritance
Is there any way to set the circular reference limit in the serializer component of Symfony (not JMSSerializer) with any config or something like that?
I have a REST Application with FOSRestBundle and some Entities that contain other entities which should be serialized too. But I'm running into circular reference errors.
I know how to set it like this:
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getName();
});
But this has to be done in more than one controller (overhead for me).
I want to set it globally in the config (.yml) e.g. like this:
framework:
serializer:
enabled: true
circular_limit: 5
Found no serializer API reference for this so I wonder is it possible or not?
For a week have I been reading Symfony source and trying some tricks to get it work (on my project and without installing a third party bundle: not for that functionality) and I finally got one. I used CompilerPass (https://symfony.com/doc/current/service_container/compiler_passes.html)... Which works in three steps:
1. Define build method in bundle
I choosed AppBundle because it is my first bundle to load in app/AppKernel.php.
src/AppBundle/AppBundle.php
<?php
namespace AppBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new AppCompilerPass());
}
}
2. Write your custom CompilerPass
Symfony serializers are all under the serializer service. So I just fetched it and added to it a configurator option, in order to catch its instanciation.
src/AppBundle/AppCompilerPass.php
<?php
namespace AppBundle;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class AppCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$container
->getDefinition('serializer')
->setConfigurator([
new Reference(AppConfigurer::class), 'configureNormalizer'
]);
}
}
3. Write your configurer...
Here, you create a class following what you wrote in the custom CompilerPass (I choosed AppConfigurer)... A class with an instance method named after what you choosed in the custom compiler pass (I choosed configureNormalizer).
This method will be called when the symfony internal serializer will be created.
The symfony serializer contains normalizers and decoders and such things as private/protected properties. That is why I used PHP's \Closure::bind method to scope the symfony serializer as $this into my lambda-like function (PHP Closure).
Then a loop through the nomalizers ($this->normalizers) help customize their behaviours. Actually, not all of those nomalizers need circular reference handlers (like DateTimeNormalizer): the reason of the condition there.
src/AppBundle/AppConfigurer.php
<?php
namespace AppBundle;
class AppConfigurer
{
public function configureNormalizer($normalizer)
{
\Closure::bind(function () use (&$normalizer)
{
foreach ($this->normalizers as $normalizer)
if (method_exists($normalizer, 'setCircularReferenceHandler'))
$normalizer->setCircularReferenceHandler(function ($object)
{
return $object->getId();
});
}, $normalizer, $normalizer)();
}
}
Conclusion
As said earlier, I did it for my project since I dind't wanted FOSRestBundle nor any third party bundle as I've seen over Internet as a solution: not for that part (may be for security). My controllers now stand as...
<?php
namespace StoreBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class ProductController extends Controller
{
/**
*
* #Route("/products")
*
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$data = $em->getRepository('StoreBundle:Product')->findAll();
return $this->json(['data' => $data]);
}
/**
*
* #Route("/product")
* #Method("POST")
*
*/
public function newAction()
{
throw new \Exception('Method not yet implemented');
}
/**
*
* #Route("/product/{id}")
*
*/
public function showAction($id)
{
$em = $this->getDoctrine()->getManager();
$data = $em->getRepository('StoreBundle:Product')->findById($id);
return $this->json(['data' => $data]);
}
/**
*
* #Route("/product/{id}/update")
* #Method("PUT")
*
*/
public function updateAction($id)
{
throw new \Exception('Method not yet implemented');
}
/**
*
* #Route("/product/{id}/delete")
* #Method("DELETE")
*
*/
public function deleteAction($id)
{
throw new \Exception('Method not yet implemented');
}
}
The only way I've found is to create your own object normalizer to add the circular reference handler.
A minimal working one can be:
<?php
namespace AppBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
class AppObjectNormalizer extends ObjectNormalizer
{
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
{
parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
$this->setCircularReferenceHandler(function ($object) {
return $object->getName();
});
}
}
Then declare as a service with a slithly higher priority than the default one (which is -1000):
<service
id="app.serializer.normalizer.object"
class="AppBundle\Serializer\Normalizer\AppObjectNormalizer"
public="false"
parent="serializer.normalizer.object">
<tag name="serializer.normalizer" priority="-500" />
</service>
This normalizer will be used by default everywhere in your project.
I have an Entity that pulls it's data from a REST web service. To keep thing consistent with the entities in my app that pull data from the database I have used ORM and overridden the find functions in the repository.
My problem is that ORM seems to demand a database table. When I run doctrine:schema:update it moans about needing an index for the entity then when I add one it creates me a table for the entity. I guess this will be a problem in the future as ORM will want to query the database and not the web service.
So... am I doing this wrong?
1, If I continue to use ORM how can I get it to stop needing the database table for a single entity.
2, If I forget ORM where do I put my data loading functions? Can I connect the entity to a repository without using ORM?
So... am I doing this wrong?
Yes. It doesn't make sense to use the ORM interfaces if you don't really want to use an ORM.
I think the best approach is NOT to think about implementation details at all. Introduce your own interfaces for repositories:
interface Products
{
/**
* #param string $slug
*
* #return Product[]
*/
public function findBySlug($slug);
}
interface Orders
{
/**
* #param Product $product
*
* #return Order[]
*/
public function findProductOrders(Product $product);
}
And implement them with either an ORM:
class DoctrineProducts implements Products
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function findBySlug($slug)
{
return $this->em->createQueryBuilder()
->select()
// ...
}
}
or a Rest client:
class RestOrders implements Orders
{
private $httpClient;
public function __construct(HttpClient $httpClient)
{
$this->httpClient = $httpClient;
}
public function findProductOrders(Product $product)
{
$orders = $this->httpClient->get(sprintf('/product/%d/orders', $product->getId()));
$orders = $this->hydrateResponseToOrdersInSomeWay($orders);
return $orders;
}
}
You can even make some methods use the http client and some use the database in a single repository.
Register your repositories as services and use them rather then calling Doctrine::getRepository() directly:
services:
repository.orders:
class: DoctrineOrders
arguments:
- #doctrine.orm.entity_manager
Always rely on your repository interfaces and never on a specific implementation. In other words, always use a repository interface type hint:
class DefaultController
{
private $orders;
public function __construct(Orders $orders)
{
$this->orders = $orders;
}
public function indexAction(Product $product)
{
$orders = $this->orders->findProductOrders($product);
// ...
}
}
If you don't register controllers as services:
class DefaultController extends Controller
{
public function indexAction(Product $product)
{
$orders = $this->get('repository.orders')->findProductOrders($product);
// ...
}
}
A huge advantage of this approach is that you can always change the implementation details later on. Mysql is not good enough for search anymore? Let's use elastic search, it's only a single repository!
If you need to call $product->getOrders() and fetch orders from the API behind the scenes it should still be possible with some help of doctrine's lazy loading and event listeners.
I have creating rest api call.
I have two table
1. person - foreign key is id
2. contact - foreign key is personId
I have creating model for this one is
class Group extends ActiveRecord
{
public function relations()
{
return array(
'user'=>array(self::HAS_ONE,'User','id'),
'contact'=>array(self::HAS_MANY,'Contact','personId')
);
}
}
Controller is
public function actionView(){
$group=Group::find();
return $group;
}
Is this enough?. It is not working for me. How to make one to many relations in yii
ActiveModel::find() returns an ActiveQuery instance, it doesn't run the query.
You either use Group::findOne(primaryKey) or Group::findAll(condition) or do return $group->one(); or return $group->all();
As for the relations (in your model):
public function getUser()
{
return $this->hasOne(User::className(), ['id', 'user']);
}
public function getContact()
{
return $this->hasMany(Contact::className(), ['personId', 'user']);
}
You call the with $group->user and $group->contact. Make sure not to use the function directly, it also returns an ActiveQuery instance, which is not your actual object.
I'm having a problem with passing the entity manager between two layers of controllers.
The system I'm building has the following structure:
2 Bundles:
Core Bundle (let's call it Backend Controller)
This is the bundle that contains all the Models (entities) and business rules/logic.
API Bundle (call it Frontend controller)
Is responsible for checking the permissions of passed in api keys and communicating with the Core bundle to get the info.
Here's an example with the User controllers and entities:
UserController.php in APIBundle:
<?php
namespace Acme\Bundle\APIBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Acme\Bundle\CoreBundle\Controller\UserController as User;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class UserController extends BaseController implements AuthenticatedController
{
public function readAction(Request $request) {
$user = new User($this->getDoctrine()->getManager());
$user->load(2);
return $this->response($user);
}
}
UserController.php in CoreBundle:
<?php
namespace Acme\Bundle\CoreBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Response;
use Acme\Bundle\CoreBundle\Entity\User;
class UserController extends BaseController
{
function __construct($em) {
parent::__construct($em);
$this->entity = new User();
}
/**
* Get userId
*
* #return integer
*/
public function getUserId()
{
return $this->entity->userId;
}
/**
* Set firstName
*
* #param string $firstName
* #return User
*/
public function setFirstName($firstName)
{
$this->entity->firstName = $firstName;
return $this;
}
// ...
public function load($id) {
if (!$this->entity instanceof \Acme\Bundle\CoreBundle\Entity\BaseEntity) {
throw new \Exception('invalid entity argument');
}
$this->entity = $this->em->getRepository('AcmeCoreBundle:User')->find($id);
}
}
Please, tell me if I'm doing this right. It seems strange to pass the entity manager between the controllers every time.
Maybe there's a better way of doing that?
Does the idea between the separation of the bundles make sense?
Thank you, every idea is greatly appreciated.
If CoreBundle UserController is never accessed through HTTP nor do its methods return instances of Symfony\Component\HttpFoundation\Response then it's not really a controller.
You should better define it as a service, as in CoreBundle\Service\User, and inject the EntityManager through the DI container.
sevices.yml
corebundle.userservice:
class: Acme\CoreBundle\Service\User
arguments: [#doctrine.orm.entity_manager]
It will then be available from Acme\Bundle\APIBundle\Controller\UserController with the following:
$user = $this->get('corebundle.userservice');
Of course, you can also define Acme\Bundle\APIBundle\Controller\UserController as a service on its own, then inject 'corebundle.userservice', for convenience.
I suggest you read the Symfony docs on Dependency Injection.
Search to get entity manager in Entity class is a wrong way !
In CoreBundle, you use the UserController.php same as an Entity class.
Read docs to understand how to properly use repository in symfony.
In UserController of APIBundle you must call a custom repository function. This repository is declare in your entity.