I have 3 tables : Profile - Permission - ProfilePermissionValue
Profile and Permission are classic entities, and ProfilePermissionValue is an association of a Profile.id, Permission.id, and an extra field representing the value of the permission for the profile.
When I add a Permission, I want a new row being inserted in ProfilePermissionValue for each Profile.
Same on reverse, when I add a new Profile, ... And same on delete by the way.
The question : Is there a way to do it with Doctrine (Symfony 3) functionalities, or I need to code it myself ?
I think you look at the permission <-> profile more strictly than you should. Basically in almost every ACL I worked with there was a assumption - when something is not allowed, it`s disallowed (or when something is not disallowed is allowed which is more dangerous). Which significantly reduce amount of data, you must save.
So when you create your entities like this
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
*/
class Permission
{
// id column
/**
* #ORM\Column(type="string")
* #var string
*/
private $name;
/**
* #return string
*/
public function getName()
{
return $this->name;
}
}
and
<?php
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
*/
class User
{
// id column
// name column
/**
* #ORM\ManyToMany(targetEntity=Permission::class)
* #ORM\JoinTable(name="allowed_permissions",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="permission_id", referencedColumnName="id")}
* )
* #var Permission[]|Collection
*/
private $allowedPermissions;
/**
* #return Permission[]
*/
public function getAllowedPermissions()
{
return $this->allowedPermissions->toArray();
}
}
you can simply implement your own class for interface AuthorizationCheckerInterface as
<?php
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class Authorizator implements AuthorizationCheckerInterface
{
/**
* #param string $name
* #param User $user
* #return bool
*/
public function isGranted($name, $user)
{
foreach ($user->getAllowedPermissions() as $permission) {
if ($permission->getName() === $name) {
return TRUE;
}
}
return FALSE;
}
}
without any needs of having deny permission in your database.
Related
I would like to serialize my objects into text fields in order to store a representation of them for traceability.
What I really want is a JSON representation of the entity's properties, and whenever there is an object, I would like a JSON representation of that as well, but only on that first level, I don't want it to dig deeper into what possible objects and relations there is below that, I'm happy with the object ID's
Many objects reference other objects, and from this documentation https://symfony.com/doc/current/components/serializer.html#handling-circular-references it appears this can be easily handled by just storing the object's ID rather than serializing the entire object (again). But in my case it doesn't work =) Am I missing something critical here?
Entity
<?php
namespace App\Entity;
use App\Repository\RegularServiceHoursRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=RegularServiceHoursRepository::class)
*/
class RegularServiceHours
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $open;
/**
* #ORM\Column(type="datetime")
*/
private $close;
/**
* #var object \App\Entity\ACRGroup
*
* #ORM\ManyToOne(targetEntity="\App\Entity\ACRGroup", inversedBy="regularServiceHours")
* #ORM\JoinColumn(name="acr_group", referencedColumnName="id", nullable=false)
*/
protected $ACRGroup;
public function getId(): ?int
{
return $this->id;
}
public function getOpen(): ?\DateTimeInterface
{
return $this->open;
}
public function setOpen(\DateTimeInterface $open): self
{
$this->open = $open;
return $this;
}
public function getClose(): ?\DateTimeInterface
{
return $this->close;
}
public function setClose(\DateTimeInterface $close): self
{
$this->close = $close;
return $this;
}
/**
* Set aCRGroup
*
* #param \App\Entity\ACRGroup $aCRGroup
*
* #return DebitPeriod
*/
public function setACRGroup(\App\Entity\ACRGroup $aCRGroup)
{
$this->ACRGroup = $aCRGroup;
return $this;
}
/**
* Get aCRGroup
*
* #return \App\Entity\ACRGroup
*/
public function getACRGroup()
{
return $this->ACRGroup;
}
/**
* Get debitTimeSeconds
*
* #return int
*/
public function getTimeSeconds()
{
$open = $this->getOpen();
$close = $this->getClose();
$r = $close->format('U') - $open->format('U');
return $r;
}
}
Controller
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Serializer;
public function log($type,$message,$unit=null,$previous=null,$current=null) {
//We only log successful operations.
//If you want to log also errors, be very careful to not have anything persisted already, in que to be flushed. The flush below will store possible entities under conflict check and entities that sent you here due to a failed validation/conflict check, entities that weren't meant to be saved but rejected and forgotten.
if ($type == 'success') {
$encoder = new JsonEncoder();
$defaultContext = [
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
return $object->getId();
},
];
$normalizer = new ObjectNormalizer(null, null, null, null, null, null, $defaultContext);
$serializer = new Serializer([$normalizer], [$encoder]);
dd($serializer->serialize($current, 'json'));
It never gets to the dump action, it chews for 30 seconds until memory is exhausted, and then it says:
Error: Maximum execution time of 30 seconds exceeded
Have I somehow missed where to set a default depth of 1? (I understood depth 1 to be the default when nothing was set).
Pay attention to Serialization Groups Attributes. With this attribute you can select the desired data when serializing an object.
Specify groups to the desired object properties:
class RegularServiceHours
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $open;
/**
* #ORM\Column(type="datetime")
* #Groups({"default"})
*/
private $close;
/**
* #var object \App\Entity\ACRGroup
*
* #ORM\ManyToOne(targetEntity="\App\Entity\ACRGroup", inversedBy="regularServiceHours")
* #ORM\JoinColumn(name="acr_group", referencedColumnName="id", nullable=false)
* #Groups({"default"})
*/
private $ACRGroup;
}
Also specify a group for the ACRGroup identifier
class ACRGroup
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
}
And just specify the group when serializing the object
$serializer->serialize($current, 'json', ['groups' => ['default']]);
I think that should solve your problem
How can I enforce uniqueness of a value within overlapping date range in Symfony using Doctrine ORM.
I have the following entity
<?php
/**
* #ORM\Entity
* #ORM\Table("tax_component")
*/
class TaxComponent
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="tax_component_id", type="integer")
*/
private ?int $id;
/**
* #ORM\Column(name="tax_component_name", type="string", length=20)
*/
private string $name;
/**
* #ORM\Column(name="tax_component_rate", type="integer")
* #Assert\GreaterThanOrEqual(0)
*/
private int $rate;
/**
* #ORM\Column(name="tax_component_applicable_from", type="datetime_immutable")
*/
private DateTimeInterface $applicableFrom;
/**
* #ORM\Column(name="tax_component_applicable_to", type="datetime_immutable")
*/
public function __construct(string $name, int $rate, ?DateTimeImmutable $applicableFrom = null, ?DateTimeImmutable $applicableTo = null)
{
...
}
}
I want to make $name unique withing overlapping time frames of $applicableFrom and $applicableTo. For example,
$repository->save(
new TaxComponent('inter-state', 1800, new DateTime('2018-04-01:00:00:00'), new DateTime('2019-03-31T23:59:59'))
);
// The following should be allowed since there is no overlap between the two time ranges using the name 'inter-state'
$repository->save(
new TaxComponent('inter-state', 1200, new DateTime('2019-04-01:00:00:00'), new DateTime('2020-03-31T23:59:59'))
);
// The following should fail since 'inter-state' is ambiguous during the period 2019-09-01:00:00:00 to 2020-03-31T23:59:59
$repository->save(
new TaxComponent('inter-state', 1800, new DateTime('2019-09-01:00:00:00'), new DateTime('2020-09-31T23:59:59'))
);
Is there a constraint to enforce this is Symfony?
I am currency planning to check for existing entities from within TaxComponentRepository::save, before calling $this->entityManager->persist. Is there a better solution?
The cleaner way would be to create your own custom assert.
Starting by creating your constraint :
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class TaxComponentConstraint extends Constraint
{
public $message = 'Another tax component overlap this one: {{ taxComponent}}';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return 'App\Validator\Constraints\TaxComponentValidator';
}
}
And now you have to create a validator that will check if there exist an overlap with two tax component.
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class TaxComponentValidator extends ConstraintValidator
{
public function validate($taxComponentObject, Constraint $constraint)
{
//Check however you want if the tax component can be created (So no overlap between two existing TaxComponent)
if($overlappingTaxComponent){
$this->context->buildViolation($constraint->message)
->setParameter('{{ taxComponent }}', $overlappingTaxComponent->__toString())
->addViolation();
}
}
}
Here, $overlappingTaxComponent is a TaxComponent preventing us from making one because of your constraint.
If the constraint is properly done, you can now use it easily in your entity so that it check automatically when submitting the form :
<?php
//...
use App\Validator\Constraints as CustomAssert;
/**
* #ORM\Entity
* #ORM\Table("tax_component")
* #CustomAssert\TaxComponentConstraint
*/
class TaxComponent
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="tax_component_id", type="integer")
*/
private ?int $id;
/**
* #ORM\Column(name="tax_component_name", type="string", length=20)
*/
private string $name;
In my application, there is a 'status' column in the 'users' table. Which indicates the user activeness. Now I want to check the activeness of the user before login to the system and give a message if he is deactivated. How to do this? There are several answers here, but I cannot make this work with the help of those answers.
This is my LoginController.php
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
}
Create a middleware class to check the status column. For example:
<?php namespace App\Http\Middleware;
use Closure;
class CheckStatusMiddleware {
/**
* Run the request filter.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$user = User::where('email', $request->input('email'))->firstOrFail();
if (!$user->active)
{
return redirect('home');
}
return $next($request);
}
}
Then register the class and apply it to the necessary route(s).
See Middleware for more information.
You can use authenticated() method.
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
/**
* The user has been authenticated.
*
* #param \Illuminate\Http\Request $request
* #param mixed $user
* #return mixed
*/
protected function authenticated(Request $request, $user)
{
// Check status
if ($user->status == 'inactive') {
$this->logout($request);
// Send message
throw ValidationException::withMessages([
$this->username() => [__('Your status is inactive')],
]);
}
}
}
i have 2 entities.
the simple logic is that a user has many delivery address, so in the future he will be able to choose one of them for their deliverys.
first entity, Direccion (address).
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* Direccion
*
* #ORM\Table(name="direccion", indexes={#ORM\Index(name="id_usuario",
columns={"id_usuario"})})
* #ORM\Entity
*/
class Direccion
{
/**
* #var string
*
* #ORM\Column(name="Calle", type="string", length=100, nullable=false)
*/
private $calle;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var \Usuario
*
* #ORM\ManyToOne(targetEntity="Usuario", inversedBy="direcciones", fetch="EAGER")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_usuario", referencedColumnName="id" )
* })
*/
private $idUsuario;
/**
* Set calle
*
* #param string $calle
* #return Direccion
*/
public function setCalle($calle)
{
$this->calle = $calle;
return $this;
}
/**
* Get calle
*
* #return string
*/
public function getCalle()
{
return $this->calle;
}
/**
* Set idUsuario
*
* #param \Usuario $idUsuario
* #return Direccion
*/
public function setIdUsuario(\Usuario $idUsuario = null)
{
$this->idUsuario = $idUsuario;
return $this;
}
/**
* Get idUsuario
*
* #return \Usuario
*/
public function getIdUsuario()
{
return $this->idUsuario;
}
}
and the second entity is, Usuario (User)
<?php
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Usuario
*
* #ORM\Table(name="usuario")
* #ORM\Entity
*/
class Usuario
{
/**
* #var string
*
* #ORM\Column(name="nombre", type="string", length=255, nullable=true)
*/
private $nombre;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Direccion", mappedBy="id_usuario", cascade={"persist"})
*/
private $direcciones;
public function __construct()
{
$this->direcciones = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Set nombre
*
* #param string $nombre
* #return Usuario
*/
public function setNombre($nombre)
{
$this->nombre = $nombre;
return $this;
}
/**
* Get nombre
*
* #return string
*/
public function getNombre()
{
return $this->nombre;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
public function getDirecciones()
{
return $this->direcciones;
}
public function setDirecciones($direcciones)
{
$this->direcciones = $direcciones;
return $this;
}
}
i have already readed many blogs and similar questions , but i can't get the property $direcciones hydrated or filled with the associated data, when i try to achieve that on the inverse side of the relation (OneToMany), with $usuario->getDirecciones();
i'm working in with this code in a non MVC architecture, to this point everything works like charm in getting creating and updating through a DAO wich uses the Doctrine sentences,so i know that in the ending points (updating, creating,persist and flush,retrieving data with find, findBy,etc), everything works fine.
when i try to fill the ArrayCollection calling from the Service layer where i use both classes (Usuario-InverseSide and Direccion-OwnerSide), nothing happens, the $direcciones arrayCollection property on Usuario instance, doesn't fetch anything, even when i tried establishing fetch="EAGER"on the annotations.
for the rest of the data or properties, everything works fine, all the other attributes of the user get filled just fine.
For some reason, the annotations are not being considered, donĀ“t know why.
i've spend a few days trying to figure out how to acomplish this way of accessing the associated data, didn't wanted to use DQL, but at this point i think i will take that road.
I tried something hard-coded to test and, the result is the same, $direcciones doesn't get his data.Obvious are already discarted, that exact ID for a Usuario(user) has his related direcciones(address) setted.
$usuario = $this->usuarioDAO->find(20);
$direcciones = $usuario->getDirecciones();
var_dump($direcciones);
return $usuario;
the relation was only defined with a FK called id_usuario on Direccion on a mysql InnoDb table.
don't know if i should be setting something else on the Usuario table.
or if there is something wrong in the way i store the entities on my project.
please help me, any recommendation will be appreciated.
doctrine 2, hwo do get data from the inverse side (many to one)
Provided that your bidirectional mapping between Direccion and Usuaro is correct, you need to use setter methods on both sides.
And the other thing, when using ArrayCollection, an easier (perhaps the only way) is to add elements one by one.
Therefore, instead of setDirecciones() you add this method to Usuario class:
public function addDireccion(Direccion $direccion) {
$direccion->setIdUsuario($this);
$this->direcciones->add($direccion);
}
Hope that helps.
And, it would be better to name it $usuario and setUsuario instead od setIdUsuario because you work with objects and PHP should not be concerned about actual database field names (to put it that way).
i resolved it.
happens that for some reason, the php annotations were not being actually readed, or on a different way to say it, it was listening to the XML annotations first, so i put the xml sentence on the inverse side of the relation (usuario) inside the tags
<one-to-many field="direcciones" target-entity="Direccion" mapped-by="idUsuario" />
and it worked, now i got polish the data result of the consult so i can send a cleaner/clearer object to the front-end.
I have 2 entities: Submission and Vote.
Submission Entity:
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Votes", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*/
protected $vote;
/**
* #return mixed
*/
public function getVote()
{
return $this->vote->toArray();
}
/**
* #param Votes $vote
* #return $this
*
*/
public function setVote(Votes $vote)
{
if (!$this->vote->contains($vote)) {
$this->vote->add($vote);
}
return $this;
}
Vote entity:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Submission", inversedBy="id")
* #ORM\JoinColumn(name="submission_id", referencedColumnName="id", nullable=true)
*/
protected $submission;
/**
* #return mixed
*/
public function getSubmission()
{
return $this->submission;
}
/**
* #param mixed $submission
*/
public function setSubmission($submission)
{
$this->submission = $submission;
}
Problem is that when i'm setting vote for previously selected submission:
$submission = $em->getRepository('AppBundle:Submission')->findOneById($submissionId);
$vote = new Votes();
$vote->setId($submission->getId());
$vote->setFeedback($judgeComment);
$vote->setScore($judgeScore);
$vote->setSubmission($submissionId);
$submission->setVote($vote);
$em->persist($submission);
$em->flush();
Vote table
submission_id column is always NULL - i'm not sure what i'm doing wrong. I want to store here submission id, for what this vote is.
You don't have to set ids in doctrine but objects, as it is an ORM
$submission = $em->getRepository('AppBundle:Submission')->findOneById($submissionId);
$vote = new Votes();
$vote->setId($submission->getId()); // <--- Are you sure that you need this?
$vote->setFeedback($judgeComment);
$vote->setScore($judgeScore);
// $vote->setSubmission($submissionId); <--- ERROR!
$vote->setSubmission($submission) // <--- Check advice below
$submission->setVote($vote);
$em->persist($submission);
$em->flush();
If I can also leave a suggestion here, modify setVote as follows
public function setVote(Votes $vote)
{
if (!$this->vote->contains($vote)) {
$this->vote->add($vote);
$vote->setSubmission($this);
}
return $this;
}
and you'll never need to set explicitly both sides of association anymore
I'm also noticing that your class name is plural (should be singular into domain) and that vote attribute into submission is singular (should be plural as it's a collection; you have a -to-Many annotation. Even the setter should be an adder as you're not setting but adding a Vote)
Thank you very much Gerry. Thanks to you problem is solved. Here is the answer:
Submission Entity:
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Votes", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*/
protected $vote;
/**
* #return mixed
*/
public function getVote()
{
return $this->vote->toArray();
}
/**
* #param Votes $vote
* #return $this
*
*/
public function setVote(Votes $vote)
{
$this->vote[] = $vote;
$vote->setSubmission($this);
return $this;
}
Vote Entity:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Submission", inversedBy="vote")
* #ORM\JoinColumn(name="submission_id", referencedColumnName="id",onDelete="cascade", nullable=true)
*/
protected $submission;
/**
* #return mixed
*/
public function getSubmission()
{
return $this->submission;
}
/**
* #param mixed $submission
*/
public function setSubmission($submission)
{
$this->submission = $submission;
}
Now when i'm adding new vote, everything is fine:
$vote = new Votes();
$vote->setFeedback($judgeComment);
$vote->setScore($judgeScore);
$submission->setVote($vote);
$em->persist($submission);
$em->flush();
result