Update another field on slug creation using DoctrineExtensios - symfony

I'm using DoctrineExtensions in my Symfony 2 project, i have a simple Entity class where i'm using Sluggable on a property, then i would like to set the value to another property based on the slug, but, even when using Lifecycle Callbacks #ORM\PrePersist, #ORM\PreFlush, at this time the slug property still empty, meaning no slug is generated yet, here is my class, to keep this short, i'm not going to put here the get and set function of each property, just the part of the class that are important for this example(please, read the comments)
<?php
namespace My\LearnBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* Banner
*
* #ORM\Table(name="banner")
* #ORM\HasLifecycleCallbacks()
*/
class Banner {
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=128, nullable=false)
* #Assert\NotBlank()
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=256, nullable=false)
* #Gedmo\Slug(fields={"name"})
*/
private $slug;
/**
* #var string
*
* #ORM\Column(name="tracking_url", type="string", length=256, nullable=false)
*/
private $trackingUrl;
/**
* Set slug
*
* #param string $slug
* #return Banner
*/
public function setSlug($slug) {
$this->slug = $slug;
$this->trackingUrl = $slug."/tracking"; //Doesn't work
return $this;
}
/**
* Set trackingUrl value
*
* #ORM\PreUpdate
*/
public function setTrackingUrlValue() {
//the slug is empty. Doesn't work
$this->trackingUrl = $this->slug."/tracking";
return $this;
}
/**
* Set trackingUrl value
*
* #ORM\PreFlush
*/
public function setTrackingUrlValueOnFlush() {
//the slug is empty. Doesn't work
return $this->setTrackingUrlValue();
}
}
What i've tried? well, using the setSlug function but it doesn't work(note comments on example above), seems it is not called. Using Lifecycle Callbacks #ORM\PrePersist, #ORM\PreFlush and #ORM\PreUpdate, doesn't work neither.
Now i solved this in the controller, after calling flush on the EntityManager, setting the property value based on the slug and calling flush again, so, making 2 database query in a single request, one for insert, one for update. I don't want to use an Event Listener because this behavior is just for this particular entity, or exist a way to attach an event listener to a single entity?.
But right now, i would like to know:
why what i was trying to do using Lifecycle Callbacks didn' work?
Why using the setSlug function didn't work?
A cleaner way to accomplish what i want?
thanks

What's probably happening is that the annotated listeners have a higher priority than the one creating the slug (or they have an equal priority in which case the annotated ones get probably added before).
I'm afraid you have to ditch annotations, create an actual listener and tag it for the event registration compiler pass to pick it up. What's nasty with this one is that the bundle seems to use onFlush for creating the slug (code).
Listener
namespace Acme\DemoBundle\Listener;
use Acme\DemoBundle\Model\TrackingUrlUpdateable;
use Doctrine\ORM\Event\OnFlushEventArgs;
class TrackingUrlUpdater
{
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uof = $em->getUnitOfWork();
// Let's process both types of entities in a single loop.
$entities = array_merge(
$uof->getScheduledEntityInsertions(),
$uof->getScheduledEntityUpdates()
);
foreach ($entities as $entity) {
// Using a fictional interface (e.g. for making testing easier).
if (!($entity instanceof TrackingUrlUpdateable)) {
continue;
}
// `Banner::updateTrackingUrl()` would internally change the
// tracking url to the correct one.
$entity->updateTrackingUrl();
// The change-set must be recomputed as its fields were modified
// in the previous step.
$uof->recomputeSingleEntityChangeSet(
$em->getClassMetadata(get_class($entity)),
$entity
);
}
}
}
Registration
What's left now is to register the listener with a lower priority than the Sluggable listener.
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
<services>
<service id="acme.listener.tracking_url" class="Acme\DemoBundle\Listener\TrackingUrlUpdater">
<tag name="doctrine.event_listener" event="onFlush" priority="-1" />
</service>
</services>
</container>
Oh, and don't forget to test!

Assuming you're using StofDoctrineExtensionsBundle to integrate the library, the alternative would be to increase the priority of the sluggable listener so that your annotated callbacks would get invoked after the sluggable listener.
A compiler pass would probably do the trick.
namespace Acme\DemoBundle\DependencyInjection\CompilerPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class SluggableListenerPriorityChangingPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$id = 'stof_doctrine_extensions.listener.sluggable';
$tag = 'doctrine.event_subscriber';
$definition = $container->getDefintion($id);
$attributes = $definition->getTag($tag);
if (!$attributes) {
throw new \LogicException("The listener (`$id`) must have a `$tag` tag.");
}
$attributes['priority'] = 10;
$definition
->clearTag($tag);
->addTag($tag, $attributes)
;
}
}
The sluggable listener service itself is registered here, if its enabled from the configuration.

Related

How to use repository method findByxx about field name with underline(_) in symfony

// This is my entity class object
use Doctrine\ORM\Mapping as ORM;
class PayOrder {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string",unique=true)
*/
private $pay_no;
};
// I use it in my function, this is the Repository
use Doctrine\ORM\EntityManager;
use Exception;
use Psr\Container\ContainerInterface;
use Doctrine\ORM\Mapping;
class PayOrderRepository extends \Doctrine\ORM\EntityRepository {
public function get( PayOrder $payOrder ) {
$pay_no=$payOrder->getPayNo();
// It will occurs a exception, how to fix it, any one can help me?
return $this->findBypayno($pay_no);
}
};
Besides that, I can't find the document to fix the problem.
I want to use the field name pay_no, and I want to use the repository
findbyxxx, but I do not how to use it correctly.
Change your property name to $payNo.
Probably in your entity look like that;
//...
/**
* #ORM\Column(type="string", length=255)
*/
$pay_no
//....
Change it like that;
//...
/**
* #ORM\Column(name="pay_no", type="string", length=255)
*/
$payNo
//....
After that,
Remove old getter/setter for $pay_no
For Symfony2 run app/console doctrine:generate:entities
For Symfony4 run bin/console make:entity --regenerate
Goodluck. If you have any question or blocker please write me.
Or just use
$this->findBy(['pay_no' => $payOrder->getPayNo());
The findByX method are just intercepted method calls - see line up 177 to 179 that are transformed to findBy calls.

Attach the user in session to the current EntityManager when writing functional tests

I'm writing a functional test for an Action entity having a relationship with the User entity:
<?php
namespace Acme\AppBundle\Entity;
/**
* Class Action
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Acme\AppBundle\Repository\ActionRepository")
*/
class Action
{
/**
* #var int
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \Acme\AppBundle\Entity\User
*
* #ORM\ManyToOne(targetEntity="\Acme\AppBundle\Entity\User", inversedBy="actions")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $createdBy;
}
User:
namespace Acme\AppBundle\Entity;
/**
* #ORM\Entity
* #ORM\Table(name="`user`")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Action", mappedBy="createdBy")
*/
private $actions;
}
And the user is setted in the controller with the following snippet:
<?php
namespace Acme\ApiBundle\Controller;
/**
*
* #Route("/actions")
*/
class ActionController extends FOSRestController
{
public function postAction(Request $request)
{
$action = new Action();
$action->setCreatedBy($this->getUser());
return $this->processForm($action, $request->request->all(), Request::METHOD_POST);
}
}
When calling the action with a REST client for example, everything works fine, the relationship between Action and User is persisted correctly.
Now, when testing the action with a functional test, the relationship is not working because of the following error:
A new entity was found through the relationship 'Acme\AppBundle\Entity\Action#createdBy' that was not configured to cascade persist operations for entity: test. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
For my functional test I need to inject a JWT and a session token because my routes are secured by a JWT and I need to have a user in session.
Here is how I inject that:
<?php
namespace Acme\ApiBundle\Tests;
class ApiWebTestCase extends WebTestCase
{
/**
* #var ReferenceRepository
*/
protected $fixturesRepo;
/**
* #var Client
*/
protected $authClient;
/**
* #var array
*/
private $fixtures = [];
protected function setUp()
{
$fixtures = array_merge([
'Acme\AppBundle\DataFixtures\ORM\LoadUserData'
], $this->fixtures);
$this->fixturesRepo = $this->loadFixtures($fixtures)->getReferenceRepository();
$this->authClient = $this->createAuthenticatedClient();
}
/**
* Create a client with a default Authorization header.
*
* #return \Symfony\Bundle\FrameworkBundle\Client
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->fixturesRepo->getReference('user-1');
$jwtManager = $this->getContainer()->get('lexik_jwt_authentication.jwt_manager');
$token = $jwtManager->create($user);
$this->loginAs($user, 'api');
$client = static::makeClient([], [
'AUTHENTICATION' => 'Bearer ' . $token,
'CONTENT_TYPE' => 'application/json'
]);
$client->disableReboot();
return $client;
}
}
Now, the issue is that the injected UsernamePasswordToken contains a User instance which is detached from the current EntityManager, thus resulting in the Doctrine error above.
I could merge the $user object in the postAction method into the EntityManager but I don't want to do that because it means I modify my working code to make a test passes.
I've also tried directling merging the $user object in my test into the EntityManager like this:
$em = $client->getContainer()->get('doctrine')->getManager();
$em->merge($user);
But it's not working either.
So now, I'm stuck, I really don't know what to do except that I need to attach the user in session back to the current EntityManager.
The error message you are getting indicates that the EntityManager contained in the test client's container doesn't know about your User entity. This leads me to believe that the way you are retrieving the User in your createAuthenticatedClient method is using a different EntityManager.
I suggest you try to use the test kernel's EntityManager to retrieve the User entity instead. You can get it from the test client's container, for example.
Thanks to your tweet, I come to complete the given answer and (try to) propose a solution,
The problem is that your user is not managed by the EntityManager, and more simply, because it's not a real existing user that is registered in database.
To get around this problem, you need to have a real (managed) user that doctrine could use for the association that your action is trying to create.
So, you can either create this user at each execution of your functional test case (and delete it when finished), or create it only once when execute the test case for the first time on a new environment.
Something like this should do the trick:
/** #var EntityManager */
private $em;
/**
*/
public function setUp()
{
$client = static::createClient();
$this->em = $client->getKernel()
->getContainer()
->get('doctrine');
$this->authClient = $this->createAuthenticatedClient();
}
/**
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->em
->getRepository('Acme\AppBundle\Entity\User')
->findOneBy([], ['id' => DESC]; // Fetch the last created
// ...
return $client;
}
That's a pity for your fixtures (that are so much sexier), but I don't see any way to attach your fixture as a real entry, as you can't interact more with the tested controller.
Another way would be to create a request to your login endpoint, but it would be even more ugly.

ManyToOne doctrine only changes the values of connected Entity instead of creating an new one

I have a big (simple) problem.
I have a user entity with a geolocation property as an manyToOne relation
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
/**
* User
*/
class User implements AdvancedUserInterface, \Serializable
{
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Location", cascade= {"persist"})
* #ORM\JoinColumn(name="location_id", referencedColumnName="id")
*/
private $geolocation;
And I have a location Entity like this:
/**
* Location
*
* #ORM\Table(name="location")
* #ORM\Entity(repositoryClass="AppBundle\Entity\Repository\LocationRepository")
*/
class Location
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #Assert\NotBlank()
* #ORM\Column(name="latitude", type="float", scale=12, precision=18)
*/
private $latitude;
/**
* #var string
*
* #Assert\NotBlank()
* #ORM\Column(name="longitude", type="float", scale=12, precision=18)
*/
private $longitude;
/**
* #var string
*
* #Assert\NotBlank()
* #ORM\Column(name="address", type="string", length=255)
*/
private $address;
The Problem is now, that I want to change (update) the location of my users. For that I have a FormType:
$builder->add('geolocation', 'jquerygeolocation', array();
The 'jquerygeolocation' FormType is a created FormType with the
data_class' => 'AppBundle\Entity\Location'
But when I want to change (update) the users location I have a big problem. I want to persist a new location if the location is even not in the database and I want to connect an existing location with the user. But instead doctrine changes only the values of the connected location.
For example:
before:
after:
As you can see, the id is the same. There was only an update. Nothing from the logic I guessed.
Can someone help me with this.
Thanks Michael.
With these relationships you would typically provide a pick list of existing locations and a separate mechanism to create a new location. With a location entity form embedded within your user form any updates will always be applied to the location entity that is already associated with the user.
If you want this to work as described in the question you will need to write some custom code in your controller (or better still in a business logic service used by the controller) to handle it.
Assuming it is the address which uniquely identifies a location then you would need something like this (after handling the request in the controller so that you have a user instance containing the submitted data):
$em = $this->getDoctrine()->getManager();
$locationRepo = $em->getRepository('AppBundle:Location');
$location = $locationRepo->findOneByAddress($user->getGeolocation()->getAddress());
if (!$location)
{
$location = new Location();
$location->setLongitude($user->getGeolocation()->getLongitude());
$location->setLatitude($user->getGeolocation()->getLatitude());
$location->setAddress($user->getGeolocation()->getAddress());
$em->persist($location);
}
$user->setLocation($location);
$em->flush($user);
Very good. This was exactly, what I guessed. But the solution was not as easy like this. In addition to your solution you have to prevent the update on the old location entity. Otherwise the old value is the same as the new location. To do this, I used a Lifecycle Callback in the entity:
Location.php
/**
* #ORM\PreUpdate
*/
public function preventUpdate(PreUpdateEventArgs $args)
{
$address = $args->getOldValue('address');
$latitude =$args->getOldValue('latitude');
$longitude = $args->getOldValue('longitude');
$locality = $args->getOldValue('locality');
$country = $args->getOldValue('country');
$locationArray = array(
'address' => $address,
'latitude' => $latitude,
'longitude' => $longitude,
'locality' => $locality,
'country' => $country
);
$this->update($locationArray);
}
with the following function inside the Location.php entity:
public function update($location)
{
$data = $location;
$this->address = $data['address'] ?: null;
$this->latitude = $data['latitude'] ?: null;
$this->longitude = $data['longitude'] ?: null;
$this->locality = $data['locality'] ?: null;
$this->country = $data['country'] ?: null;
}
The problem is now, that I never can be able to update a Location.
Ok, I donĀ“t now yet if I will need it.
But I would be grateful if there is another solution without this negative aspect.

How to use the registration procedure of FOSUserBundle from a third controller

I have to persist an entity (let's call it Entity for simplicity) in the database that has to be referenced to a User handled with FOSUserBundle. To make this reference I have a column entity_table.userId.
When the new Entity is created, I have to:
Create the User through the registration procedure of FosUserBundle;
Get the ID of the new created User: [meta code] $userId = $get->newCreatedUserId();
Set this id in Entity: $entity->setUserId($userId);
Persist the Entity to the database.
How can I integrate the registration procedure of FosUserBundle into the controller that persists my Entity?
MORE DETAILS
In the first time I tried to simply copy the code from the method registerAction() of the RegistrationController of FOSUserBundle: a quick and dirty approach that, anyway didn't work as i get an error as the User class i passed was wrong (I passed my custom User entity I use to overwrite the bundle).
This kind of approach has also other drawbacks:
I cannot control the registration procedure (send or decide to not send confirmation e-mails, for example);
I cannot use the builtin checks on passed data;
I cannot be sure that on FOSUserBundles updates my custom method continue to work
Others I cannot imagine at the moment...
So, I'd like to create the user in the cleanest way possible: how can i do this? Which should be a good approach?
A controller forwarding?
Anyway, an "hardcoded" custom method that emulates the registerAction() method?
A custom registration form?
I have read a lot of discussions here at StackOverflow and on Internet, I read the documentation of FOSUserBundle and of Symfony too, but I cannot decide for the good approach, also because I'm not sure I have understood all the pros and cons of each method.
If someone can put me on the right way... :)
SOMETHING MORE ABOUT MY REGISTRATION FLOW
I have a getStarted procedure handled by the controller GetStarteController.
In it I have two methods:
indexAction(), that displays a registration form with only the field "email";
endAction(), that receive the form and creates a Company using the passed e-mail (it gets the domain part only of the email).
HERE IS A WORKING MESSY CODE (inside it for Companies and Stores are called some methods that exists in the source code but are not in the posted classes below, as setBrand() or setUrl(), for example).
// AppBundle/Controller/getStartedController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use MyVendor\UserBundle\Entity\User;
use AppBundle\Entity\Companies;
use AppBundle\Entity\Stores;
class GetStartedController extends Controller
{
/**
* #Route("getstarted")
* #Template()
*/
public function indexAction()
{
$data = array();
$form = $this->createFormBuilder($data, array(
'action' => $this->generateUrl('getStartedEnd'),
))
->add('email', 'email')
->add('submit', 'submit')
->getForm();
return array(
'form' => $form->createView(),
);
}
/**
* #Route("getstarted/end", name="getStartedEnd")
* #Template()
*/
public function endAction(Request $request)
{
$form = $this->createFormBuilder()
->add('email', 'email')
->add('submit', 'submit')
->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
$data = $form->getData();
} else {
/** #todo here we have to raise some sort of exception or error */
echo 'no data submitted (See the todo in the code)';exit;
}
// Pass the email to the template
$return['email'] = $data['email'];
// Get the domain part of the email and pass it to the template
$domain = explode('#', $data['email']);
$return['domain'] = $domain[1];
// 1) Create the new user
$user = new User();
// Get the token generator
$tokenGenerator = $this->container->get('fos_user.util.token_generator');
$user->setEmail($return['email']);
$userRandomUsername = substr($tokenGenerator->generateToken(), 0, 12);
$user->setUsername('random-' . $userRandomUsername);
$plainPassword = substr($tokenGenerator->generateToken(), 0, 12);
$encoder = $this->container->get('security.password_encoder');
$encoded = $encoder->encodePassword($user, $plainPassword);
// Set the password for the user
$user->setPassword($encoded);
/** #var $userManager \FOS\UserBundle\Model\UserManagerInterface */
$userManager = $this->get('fos_user.user_manager');
// Perstist the user in the database
$userManager->updateUser($user);
$userId = $user->getId();
// 2) Create the Company object
$company = new Companies();
$company->setBrand($return['domain'])
->setAdded(new \DateTime())
->setOwnerId($userId);
// 3) Create the Store object
$store = new Stores();
$store->setEmail($return['email'])
->setUrl($return['domain'])
->setAdded(new \DateTime());
// Get the Entity Manager
$em = $this->getDoctrine()->getManager();
// Persist Company and get its ID
$em->persist($company);
$em->flush();
$return['companyId'] = $company->getId();
// Set the property branchOf of the Store object
$store->setBranchOf($return['companyId']);
// Persist the Store object
$em->persist($store);
$em->flush();
$return['storeId'] = $store->getId();
return $return;
}
}
Here the User Entity that ovewrites the one provided by FOSUserBundle
// MyVendor/UserBundle/Entity/User.php
namespace MyVendor\UserBundle\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="prefix_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
// your own logic
}
}
Some essential code of Companies.php
// AppBundle/Entity/Companies.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Companies
*
* #ORM\Table(name="companies")
* #ORM\Entity
*/
class Companies
{
/**
* #var integer
*
* #ORM\Column(name="ownerId", type="integer", nullable=false)
*/
private $ownerid;
/**
* Set ownerid
*
* #param integer $ownerid
* #return Companies
*/
public function setOwnerid($ownerid)
{
$this->ownerid = $ownerid;
return $this;
}
/**
* Get ownerid
*
* #return integer
*/
public function getOwnerid()
{
return $this->ownerid;
}
}
Some essential code of Stores.php
// AppBundle/Entity/Stores.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Stores
*
* #ORM\Table(name="stores", uniqueConstraints={#ORM\UniqueConstraint(name="branchOf", columns={"branchOf"})})
* #ORM\Entity
*/
class Stores
{
/**
* #var integer
*
* #ORM\Column(name="branchOf", type="integer", nullable=false)
*/
private $branchof;
/**
* Set branchof
*
* #param integer $branchof
* #return Stores
*/
public function setBranchof($branchof)
{
$this->branchof = $branchof;
return $this;
}
/**
* Get branchof
*
* #return integer
*/
public function getBranchof()
{
return $this->branchof;
}
}
You can use a custom registration form but the best way is clearly to listen to registration event dispatched by FOSUser.
Here is an example :
class RegistrationListener implements EventSubscriberInterface
{
/**
* L'entity manager
*
* #var EntityManager
*/
private $em;
/**
* Constructeur de l'EventListener
*
* #param \Doctrine\ORM\EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->em = $entityManager;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_INITIALIZE => 'onRegistrationInit',
);
}
/**
* Triggered when FOSUserEvents::REGISTRATION_INITIALIZE is caught.
*
* #param \FOS\UserBundle\Event\UserEvent $userEvent
*/
public function onRegistrationInit(UserEvent $userEvent)
{
$user = $userEvent->getUser();
// Define your own logic there
}
}
Don't forget to make this listener a service:
#services.yml
services:
oe_user.registration:
class: OrienteExpress\UserBundle\EventListener\RegistrationListener
# arguments are optional but you still can need them
# so I let the EM as example which is an often used parameter
arguments:
entityManager: "#doctrine.orm.entity_manager"
tags:
- { name: kernel.event_subscriber }
You'll find the complete list of event dispatched by FOSUser here
Moreover, Symfony entities are a model of objects. That said, you need to understand that you don't work with ids within your model, but object.
You should not use thing such as $var->setUserId() within entites. Doctrine is there to manage your relations, so be carefull about this. You might face unexpected problem by not using Symfony & Doctrine the way it has been designed for.
EDIT:
In your company entity, your relation is beetween a Company and a User objects. That means you dont need a User id in your company but just a instance of User.
I think you might go back to the basics before wanting to do advanced stuff.
Your relation beetween the user and the company should not be designed by an integer attribute but a real doctrine relation.
Ex:
class Company {
/**
* #ORM\ManyToOne(targetEntity="Path\To\User")
* #ORM\JoinColumn(nullable=false)
*/
private $owner;
/**
* #param $user User
*/
public function setUser(User $user)
{
$this->user = $user;
}
}
Then when you'll create a new company. You won't need to know the User's id or even insert it to make the link between them. But if you are not aware yet of this, once again, I think you should go back to the basics of Symfony since this is one of the most (maybe the most) important feature to master.

Many-to-Many, Doctrine's Entity Generator and Pluralization

Doctrine's many-to-many logic is confusing me a bit. I have a pretty simple many-to-many relationship of recipes to categories. My base entity classes are equally simple.
The Recipe entity class...
class Recipe
{
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="categories")
* #ORM\JoinTable(name="recipe_category")
**/
private $categories;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255)
*/
private $title;
public function __construct() {
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
}
And the Category entity class...
class Category
{
/**
* #ORM\ManyToMany(targetEntity="Recipe")
**/
private $recipes;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
public function __construct() {
$this->recipes = new \Doctrine\Common\Collections\ArrayCollection();
}
}
Seems pretty strait forward and matches Doctrine (and Symfony2's) documentation examples. The strange behavior comes when I try and generate the getters and setters for these classes via the Symfony console app.
The relationship setters/getters are incorrect. Take, for instance, the Category setter in the Recipe class that's generated...
/**
* Add categories
*
* #param \Namespace\CookbookBundle\Entity\Category $categories
* #return Recipe
*/
public function addCategorie(\Namespace\CookbookBundle\Entity\Category $categories)
{
$this->categories[] = $categories;
return $this;
}
It looks like the auto-generation of the method name is off. It should be "addCategory" and should be passed a "category."
While I can just correct this manually, if I re-run the entity generator, it will just add them again.
Am I doing this incorrectly or is this just a quirk of the entity generator? Can I specify an over-ride via annotation?
You're not doing anything wrong as that's how symfony generates them. I usually don't use the app/console to generate them as currently they're not doing a good job. One example is as you've mentioned the pluralization of words as you've mentioned. Another obvious one is the fact that it's using the [] notation which is pretty much treating an ArrayCollection object as a PHP array. You should never treat ArrayCollections as arrays.
This is how I have implemented it myself:
public function addCategory(Category $category)
{
if (!$this->categories->contains($category)
$this->categories->add($category);
return $this;
}
Which doesn't add duplicates to the Array collection if it's already added. Same thing goes with remove:
public function removeCategory(Category $category)
{
if ($this->categories->contains($category)
$this->categories->remove($category);
}
What I've run into many times is let's say you have 4 categories and you add and remove them
$r = new Recipe();
$c1 = new Category();
$c2 = new Category();
$r->addCategory($c1);
$r->addCategory($c2);
// at this point $r->getCategories()->toArray()[0] contains $c1
// and $r->getCategories()->toArray()[1] contains $c2
$r->removeCategory($c1);
// now $r->getCategories()->toArray()[0] is empty and
// $r->getCategories()->toArray()[1] contains $c2 still
// so in order to get the first category you need to:
$r->getCategories()->first();
You are not doing anything wrong. It is just that Doctrine automatically tries to singularize the names of method stubs whenever there is a plural name for a collection property. This is the function that Doctrine calls when you run the command doctrine:generate:entities:
$methodName = Inflector::singularize($methodName);
In your case, Doctrine tries to 'singularize' the word categories but fails to recognize the singular form correctly, so it just removes an 's' from the end returning categorie.
Also, as you see, Doctrine does not singularize the parameter passed to the method stubs, leaving $categories instead of being consistent and modifying it to $categorie.
If you want to avoid this, either you do not use plural words for collections, or use plural words and change the methods afterwards. As #keyboardSmasher comments to your post, doctrine won't overwrite methods you already have when using the command doctrine:generate:entities, and wrong methods won't hurt much if left there alone.
A final note: using ArrayCollections as arrays is perfectly fine, so this code is correct:
$this->categories[] = $category;
ArrayCollection object implements Collection, which in turn implements ArrayAccess. It is done this way precisely to be able to use ArrayCollections as Arrays.

Resources