I have a doubt about code organization using symfony3 and doctrine: I'll try to explain as clear as I can. Let's say I have a FootballClub entity:
class FootballClub
{
// other code
private $memberships;
public function addMembership(Membership $membership) : FootballClub
{
$this->memberships[] = $membership;
return $this;
}
public function removeMembership(Membership $membership) : bool
{
return $this->memberships->removeElement($membership);
}
}
The entity is in a many-to-one relationship with another entity, Membership, which represents the contract a player has with the club. Let's say each club
has only a limited number of membership it can acquire, number that is represented as a setting, for example, as a property in a Setting entity.
The question is: how should I reference that setting when removing a membership from the club and check that is respected? Entities should not have any dependency, so what would be the correct way to implement this? A service? can you provide an example? Thank you for your time.
You could create a Settings entity, linked in OneToOne relation with FootballCluc entity.
Define Settings like this and instanciate it in the FootballClub's constructor
Settings entity
/** #Entity */
class Settings
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="integer")
*/
private $maxMembership;
// Other configurable properties ...
__constructor($maxMembership = 50)
{
$this->maxMembership = $maxMembership;
}
public function getMaxMembership()
{
return $this->maxMembership;
}
public function setMaxMembership($maxMembership)
{
$this->maxMembership = $maxMembership;
}
}
Football Entity
class FootballClub
{
/**
* One FootballClub has One Settings.
* #OneToOne(targetEntity="Settings")
* #JoinColumn(name="settings_id", referencedColumnName="id")
*/
private $settings;
// other code
private $memberships;
__constructor(Settings $settings = null)
{
if (null === $settings) {
$settings = new Settings();
}
$this->settings = $settings;
}
public function addMembership(Membership $membership) : FootballClub
{
if ($this->settings->getMaxMembership() <= count($this->memberships)) {
// throw new Exception("Max number of membership reached"); Strict mode
// return false // soft mode
}
$this->memberships-> = $membership;
return $this;
}
public function removeMembership(Membership $membership) : bool
{
return $this->memberships->removeElement($membership);
}
}
Related
This is one is a bit weird
I'm using symfony3/php7
I have the following ProUser entity linked to a Organization entity, used to identity pro account, (important part is the "isEnabled" method), when I try to login with a ProUser that has a linked Organization (they all have, but I made triple sure to choose one that had in database), I got an error that the organization is null, but if i had a dump method to debug, then the organization is correctly retrieved from database by doctrine...
/**
* Represent a professional owner (i.e a theater owner etc.)
*
* #ORM\Entity
* #ORM\Table(name="pro_user")
*/
class ProUser implements AdvancedUserInterface, \Serializable
{
/**
* #ORM\Column(name="id", type="guid")
* #ORM\Id
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Organization", cascade={"persist"}, mappedBy="legalRepresentative")
*/
private $organization;
public function getOrganization()
{
return $this->organization;
}
public function setOrganization(Organization $organization)
{
$this->organization = $organization;
return $this;
}
/**
* Note: needed to implement the UserInterface
*/
public function getUsername()
{
return $this->email;
}
// for AdvancedUserInterface
public function isEnabled(): bool
{
$organization = $this->getOrganization();
// when this line is not present,
// it throws an exception that $organization is null,
// no problem when this line is present
dump($organization);
return $organization->isValidated();
}
public function isAccountNonExpired()
{
return true;
}
public function isAccountNonLocked()
{
return true;
}
public function isCredentialsNonExpired()
{
return true;
}
}
The stacktrace :
Symfony\Component\Debug\Exception\FatalThrowableError:
Call to a member function isValidated() on null
at src/AppBundle/Entity/ProUser.php:151
at AppBundle\Entity\ProUser->isEnabled()
(vendor/symfony/symfony/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php:277)
at Symfony\Component\Security\Core\Authentication\Token\AbstractToken->hasUserChanged(object(ProUser))
(vendor/symfony/symfony/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php:101)
at Symfony\Component\Security\Core\Authentication\Token\AbstractToken->setUser(object(ProUser))
(vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/ContextListener.php:176)
at Symfony\Component\Security\Http\Firewall\ContextListener->refreshUser(object(RememberMeToken))
(vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/ContextListener.php:109)
at Symfony\Component\Security\Http\Firewall\ContextListener->handle(object(GetResponseEvent))
(vendor/symfony/symfony/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php:46)
at Symfony\Bundle\SecurityBundle\Debug\WrappedListener->handle(object(GetResponseEvent))
(vendor/symfony/symfony/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php:35)
at Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener->handleRequest(object(GetResponseEvent), object(RewindableGenerator))
(vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall.php:56)
at Symfony\Component\Security\Http\Firewall->onKernelRequest(object(GetResponseEvent))
(vendor/symfony/symfony/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallListener.php:48)
Is it due to the code happening in the Security Component, and the entity was unserialized instead of being retrieved by doctrine, so that getOrganization() does not yet return a doctrine proxy ?
This is because of Doctrine's lazy loading of relations (it basically only knows the primary ids of the connected entities untill one or more of them are called, like with dump()).
You can add the fetch attribute to your mapping, where LAZY is default, you can set this to EAGER.
First of I all, I created the whole example below specifically for this question because the actual example is very big so if it looks stupid then assume that it is not for now!
I'm trying to come up with a solution so that I can call a correct private method (bankA() or bankB()) in controller if the validation successfully passes. As you can see in the custom validation constraint, I only check the $bank->code property however the condition is not actually that simple (there is repository checks so on) - (as I said above, it is trimmed down version). So, could please someone tell me, how will I know that which private method I should call in controller after successful validation? I'm happy to create dedicated validators if necessary so open for suggestions and examples.
Note: I looked into symfony group validation documentation but didn't really get the picture how I could apply to my scenario.
EXAMPLE REQUEST
{ "id": 66, "code": "A" }
{ "id": 34, "code": "B" }
CONTROLLER
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* #Route("bank", service="application_frontend.controller.bank")
*/
class BankController extends Controller
{
private $validator;
public function __construct(
ValidatorInterface $validator
) {
$this->validator = $validator;
}
/**
* #param Request $request
*
* #Route("")
* #Method({"POST"})
*
* #throws Exception
*/
public function indexAction(Request $request)
{
$content = $request->getContent();
$content = json_decode($content, true);
$bank = new Bank();
$bank->id = $content['id'];
$bank->code = $content['code'];
$errors = $this->validator->validate($bank);
if (count($errors)) {
throw new Exception($errors[0]->getMessage());
}
// OK, validation has passed so which one do I call now ?!?!
$this->bankA($bank);
$this->bankB($bank);
}
private function bankA(Bank $bank)
{
// Do something nice with Bank
}
private function bankB(Bank $bank)
{
// Do something bad with Bank
}
}
BANK MODEL
use Application\FrontendBundle\Validator\Constraint as BankAssert;
/**
* #BankAssert\Bank
*/
class Bank
{
/**
* #var int
*/
public $id;
/**
* #var string
*/
public $code;
}
CUSTOM VALIDATOR
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class Bank extends Constraint
{
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return get_class($this).'Validator';
}
}
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class BankValidator extends ConstraintValidator
{
public function validate($bank, Constraint $constraint)
{
if ($bank->code == 'A') {
return;
}
if ($bank->code == 'B') {
return;
}
$this->context->buildViolation('Bank error')->addViolation();
}
}
Depending on how many codes there are you could either do...
if ('A' === $bank->getCode()) {
$this->bankA($bank);
} else {
$this->bankB($bank);
}
Or..
$method = 'bank'.$bank->getCode();
if (!method_exists($this, $method)) {
throw new \Exception('Method "'.$method.'" does not exist');
}
$this->$method();
All of that being said, it would be advisable to move all of this work into a dedicated service rather than in your controller. Then in your controller use something like...
$this->container->get('do_something_to_bank.service')->processAction($bank);
I have 2 entities, Contact and ContactType.
The owner entity is Contact, with a property $type :
/**
* #ORM\ManyToOne(targetEntity="Evo\BackendBundle\Entity\ContactType")
* #ORM\JoinColumn(nullable=true)
*/
protected $type = null;
I now have to set this relation to be mandatory. I tried the following :
/**
* #ORM\ManyToOne(targetEntity="Evo\BackendBundle\Entity\ContactType")
* #ORM\JoinColumn(nullable=false)
*/
protected $type = 2;
But I get an error, which is pretty logic. I should set an entity (with id 2) as default, not a integer. But I have no idea how to do this. I previously read I shouldn't do any query to DB or any use of EntityManager inside an entity. So how can I set a default ContactType ?
A better solution probably would be to put this logic in some kind of "manager" service, for example a ContactManager.
<?php
use Doctrine\ORM\EntityManagerInterface;
class ContactManager
{
private $manager;
public function __construct(EntityManagerInterface $manager)
{
$this->manager = $manager;
}
public function createContact(ContactType $type = null)
{
if (!$type instanceof ContactType) {
$type = $this->manager->getReference('ContactType', 2);
}
return new Contact($type);
}
}
Then define your service (for example in services.yml):
contact_manager:
class: ContactManager
arguments: [#doctrine.orm.entity_manager]
In a Symfony2 project using Doctrine2. I have a Lead entity related with Promotion entity 1-N. A Lead con have a related Promotion or not.
//Lead.php
...
/**
* #var string $promotionCode
* #ORM\Column(name="promotion_code", type="string", length=16)
*/
private $promotionCode;
/**
* #var Promotion $promotion
* #ORM\ManyToOne(targetEntity="Promotion")
* #ORM\JoinColumn(name="promotion_code", referencedColumnName="id")
*/
private $promotion;
...
public function setPromotionCode($promotionCode) {
$this->promotionCode = $promotionCode;
}
public function getPromotionCode() {
return $this->promotionCode;
}
public function setPromotion($promotion) {
$this->promotion = $promotion;
}
public function getPromotion() {
return $this->promotion;
}
When I want to obtain the related promotion (if any) y do
$lead = $em->getRepository('BuvMarketplaceBundle:Lead')->find($id);
$promotion = $lead->getPromotion();
If the lead has a promotion this is OK. But if not this code returns a Promotion "entity", but when I try to use if I get an EntityNotFoundException.
So I have to test if the related promotion exists like this:
if (is_object($promotion) && method_exists($promotion, 'getDiscount')) {
try {
$promotion->getDiscount();
} catch(EntityNotFoundException $e) {
$promotion = null;
}
} else {
$promotion = null;
}
I know that I can use a findBy in the Promotion Repository, and may be another methods to check this.
But the question is if this is a bug or a feature in Doctrine2, so I'm getting a "false entity" when I think it may be a null.
I have a fairly common use case which I am trying implement but am running into some issues with the Symfony Sonata Admin Bundle (ORM). My model has a relationship between a Facility and a Sport which is based on three entity classes: Sport, Facility and SportsFacility. I followed the example http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/form_field_definition.html#advanced-usage-one-to-many and defined in the following classes (relevant parts only).
class Sport {
/**
* Bidirectional - Many facilies are related to one sport
*
* #ORM\OneToMany(targetEntity="SportsFacility", mappedBy="sport")
*/
protected $facilities;
public function getFacilities() {
return $this->facilities;
}
public function setFacilities($facilities) {
$this->facilities = $facilities;
return $this;
}
}
class Facility {
/**
* Bidirectional - Many sports are related to one facility
*
* #ORM\OneToMany(targetEntity="SportsFacility", mappedBy="facility")
*/
protected $sports;
public function getSports() {
return $this->sports;
}
public function setSports($sportsFacilities) {
$this->sports = $sportsFacilities;
return $this;
}
public function addSports($sportsFacilities) {
$this->sports = $sportsFacilities;
return $this;
}
}
class SportsFacility {
/**
* #var integer $sportsFacilityId
*
* #ORM\Column(name="sportsFacilityId", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $sportsFacilityId;
/**
* Bidirectional - Many Sports are related to one Facility (OWNING SIDE)
*
* #ORM\ManyToOne(targetEntity="Sport", inversedBy="facilities"))
* #ORM\JoinColumn(name="sportId", referencedColumnName="sportId"))
*/
protected $sport;
/**
* Bidirectional - Many Facilities are related to one Sport (OWNING SIDE)
*
* #ORM\ManyToOne(targetEntity="Facility", inversedBy="sports"))
* #ORM\JoinColumn(name="facilityId", referencedColumnName="facilityId"))
*/
protected $facility;
public function getSportsFacilityId() {
return $this->sportsFacilityId;
}
public function setSportsFacilityId($sportsFacilityId) {
$this->sportsFacilityId = $sportsFacilityId;
return $this;
}
public function getSport() {
return $this->sport;
}
public function setSport($sport) {
$this->sport = $sport;
return $this;
}
public function getFacility() {
return $this->facility;
}
public function setFacility($facility) {
$this->facility = $facility;
return $this;
}
}
In my FacilityAdmin class I have:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('name')
->with('Sports')
->add('sports', 'sonata_type_collection',
array('by_reference' => false),
array(
'edit' => 'inline',
'inline' => 'table',
))
->end();
}
When I try to add a new relation, I get the following error:
Expected argument of type "array or \Traversable", "Clarity\CoachTimeBundle\Entity\SportsFacility" given in "vendor/sonata-project/admin-bundle/Sonata/AdminBundle/Form/EventListener/ResizeFormListener.php at line 88"
Finally found where we have the problem; in your class Facility you added the missing method for sonata, but it shouldn't do that you think it should do (yup yup) :
class Facility {
...
/**
* Alias for sonata
*/
public function addSports($sportFacility) {
return $this->addSport($sportFacility);
}
}
I presume addSport() is the default doctrine generated method to add new instance of SportsFacilityto collection.
This is due to how sonata generate the method to add a new entity that is different how doctrine do :
//file: Sonata/AdminBundle/Admin/AdminHelper.php
public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
{
...
$method = sprintf('add%s', $this->camelize($mapping['fieldName']));
...
}
Someone got the same problem but the documentation didn't evolve