Вот мой сценарий:
У меня есть сущность Article. У каждой статьи есть владелец (User). Пользователь может владеть множеством статей. Пользователь может опубликовать статью через API.
Я хочу, чтобы столбец user_id для статьи устанавливался автоматически на основе токена носителя (я использую аутентификацию JWT).
Я нигде не могу найти документации о том, как это сделать. Может кто-нибудь помочь с тем, как этого добиться?
Примечание: я ищу решения, которые позволили бы избежать использования дополнительных расширений (или контроллеров) в Symfony, если это возможно. Я считаю, что Api Platform сможет достичь этого с помощью встроенных технологий, но могу ошибаться.
Вот мои сущности:
Пользователь:
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
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\UserInterface;
/**
* @ApiResource()
* @ORM\Table(name = "users")
* @ORM\Entity(repositoryClass = "App\Repository\UserRepository")
* @UniqueEntity(fields = "email", message = "Email already taken")
*/
class User implements UserInterface, \Serializable
{
/**
* @ORM\Column(type = "integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy = "AUTO")
*/
private $id;
/**
* @var string $password
*
* @ORM\Column(type = "string", length=64)
* @Assert\NotBlank()
*/
private $password;
/**
* @var string $plainPassword
*
* @Assert\NotBlank()
* @Assert\Length(max=4096)
*/
private $plainPassword;
/**
* @var string $email
*
* @ORM\Column(type = "string", length=254, unique=true)
* @Assert\NotBlank()
* @Assert\Email()
*/
private $email;
/**
* @var bool $isActive
*
* @ORM\Column(name = "is_active", type = "boolean")
*/
private $isActive;
/**
* @ORM\OneToMany(targetEntity = "Article", mappedBy = "user")
*/
private $articles;
/**
* @ORM\Column(type = "array")
*/
private $roles;
public function __construct($email)
{
$this->isActive = true;
$this->email = $email;
$this->articles = new ArrayCollection();
}
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getUsername()
{
return $this->email;
}
/**
* @return string
*/
public function getEmail()
{
return $this->email;
}
/**
* @param string $email
*
* @return $this
*/
public function setEmail($email)
{
$this->email = $email;
return $this;
}
/**
* @return null|string
*/
public function getSalt()
{
return null;
}
/**
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* @param string $password
*
* @return $this
*/
public function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* @return array
*/
public function getRoles()
{
return ['ROLE_USER'];
}
public function eraseCredentials()
{
}
/** @see \Serializable::serialize() */
public function serialize()
{
return serialize(array(
$this->id,
$this->email,
$this->password,
));
}
/** @see \Serializable::unserialize()
* @param $serialized
*/
public function unserialize($serialized)
{
list (
$this->id,
$this->email,
$this->password,
) = unserialize($serialized, array('allowed_classes' => false));
}
}
Статья
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* A User's article
*
* @ORM\Table(name = "articles")
* @ApiResource(
* attributes = {"access_control" = "is_granted('ROLE_USER')"},
* collectionOperations = {
* "get",
* "post" = {"access_control" = "is_granted('ROLE_USER')"}
* },
* itemOperations = {
* "get" = {"access_control" = "is_granted('ROLE_USER') and object.owner == user"}
* }
* )
* @ORM\Entity
* @ORM\HasLifecycleCallbacks()
*/
class Article
{
/**
* @var int $id
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type = "integer")
*/
private $id;
/**
* @var string $user
*
* @ORM\ManyToOne(targetEntity = "User", inversedBy = "articles")
*/
private $user;
/**
* @var string $name
*
* @ORM\Column(type = "text")
* @Assert\NotBlank()
*/
private $name;
/**
* @var string $location
*
* @ORM\Column(type = "text")
*/
private $location;
/**
* @var \DateTimeInterface $createdAt
*
* @ORM\Column(type = "datetime_immutable")
*/
private $createdAt;
/**
* @var \DateTimeInterface $updatedAt
*
* @ORM\Column(type = "date_immutable", nullable=true)
*/
private $updatedAt;
/**
* @ORM\PrePersist()
*/
public function setCreatedAt()
{
$this->createdAt = new \DateTime();
}
/**
* @ORM\PreUpdate()
*/
public function setUpdatedAt()
{
$this->updatedAt = new \DateTime();
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @param int $id
*/
public function setId(int $id): void
{
$this->id = $id;
}
/**
* @return string
*/
public function getUser(): string
{
return $this->user;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
/**
* @return string
*/
public function getLocation(): string
{
return $this->location;
}
/**
* @param string $location
*/
public function setLocation(string $location): void
{
$this->location = $location;
}
}






Это должно быть возможно при использовании EventListener: https://api-platform.com/docs/core/events
С их помощью вы можете подключиться к внутреннему процессу ApiPlatform без нового контроллера. Идеально подходит для вашего использования.
Реализация может выглядеть так:
<?php
// api/src/EventSubscriber/AddOwnerToArticleSubscriber.php
namespace App\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use App\Entity\Article;
use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
final class AddOwnerToArticleSubscriber implements EventSubscriberInterface
{
/**
* @var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['attachOwner', EventPriorities::PRE_WRITE],
];
}
public function attachOwner(GetResponseForControllerResultEvent $event)
{
$article = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if (!$article instanceof Article || Request::METHOD_POST !== $method) {
// Only handle Article entities (Event is called on any Api entity)
return;
}
// maybe these extra null checks are not even needed
$token = $this->tokenStorage->getToken();
if (!$token) {
return;
}
$owner = $token->getUser();
if (!$owner instanceof User) {
return;
}
// Attach the user to the not yet persisted Article
$article->setUser($owner);
}
}
Другой способ - atlantic18.github.io/DoctrineExtensions/doc/blameable.html, но я рекомендую явный способ, как указано выше. Слушатели доктрины, как правило, запутываются, я всегда стараюсь избегать их и сам моделирую ограничения предметной области. Также облегчает тестирование, так как вы можете тестировать поведение без сохранения.
@ Wildcard27, нет способа автоматизировать это. не имеет ничего общего с платформой api как таковой. объект не должен зависеть от контейнера службы, но вы должны установить данные (в данном случае пользователя) извне. плюс: ваше требовательное отношение звучит для меня немного недружелюбно в таких проектах с открытым исходным кодом, как платформа api.
@ Wildcard27 Принять мой ответ как решение проблемы?
@mblaettermann Я как раз реализую это сейчас. Извините за задержку. После тестирования я прокомментирую или помечу как принятый
@LBA Спасибо за комментарий, я перефразировал вопрос. Я вовсе не собирался показаться требовательным. Я ценю то, что люди уделяют время тому, чтобы помочь.
Обратите внимание, что начиная с Symfony 4.3 GetResponseForControllerResultEvent был переименован в ViewEvent
Вы можете создать объект с именем Base и иметь в этом классе некоторые свойства, такие как createdBy, modifiedBy, createdAt, modifiedAt, status.
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ORM\MappedSuperclass()
*/
class Base implements PublishedInfoEntityInterface
{
/**
* @ORM\Column(type = "datetime", nullable=true)
*/
private $createdAt;
/**
* @ORM\Column(type = "datetime", nullable=true)
*/
private $modifiedAt;
/**
* @ORM\ManyToOne(targetEntity = "User")
* @ORM\JoinColumn(nullable=true)
*/
private $createdBy;
/**
* @ORM\ManyToOne(targetEntity = "User")
* @ORM\JoinColumn(nullable=true)
*/
private $modifiedBy;
/**
* @ORM\Column(type = "integer", nullable=true, length=2)
*/
private $status;
public function getCreatedAt()
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): PublishedInfoEntityInterface
{
$this->createdAt = $createdAt;
return $this;
}
public function getModifiedAt()
{
return $this->modifiedAt;
}
public function setModifiedAt(\DateTimeInterface $modifiedAt): PublishedInfoEntityInterface
{
$this->modifiedAt = $modifiedAt;
return $this;
}
/**
* @return User
*/
public function getCreatedBy()
{
return $this->createdBy;
}
/**
* @param User $createdBy
* @return Base
*/
public function setCreatedBy($createdBy): self
{
$this->createdBy = $createdBy;
return $this;
}
/**
* @return User
*/
public function getModifiedBy()
{
return $this->modifiedBy;
}
/**
* @param User $modifiedBy
* @return Base
*/
public function setModifiedBy($modifiedBy): self
{
$this->modifiedBy = $modifiedBy;
return $this;
}
/**
* @return int
*/
public function getStatus()
{
return $this->status;
}
/**
* @param integer $status
*/
public function setStatus($status): void
{
$this->status = $status;
return $this;
}
}
<?php
namespace App\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use App\Entity\Base;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;
class AuthoredEntitySubscriber implements EventSubscriberInterface
{
private $entityManager;
/**
* @var Security
*/
private $security;
public function __construct(EntityManagerInterface $entityManager,Security $security)
{
$this->entityManager = $entityManager;
$this->security = $security;
}
public static function getSubscribedEvents()
{
return [KernelEvents::VIEW => ['setAuthor', EventPriorities::PRE_WRITE]];
}
public function setAuthor(ViewEvent $event)
{
$entity = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
$role = $this->security->getToken()->getRoleNames();
if (!$entity instanceof Base || !in_array($method, [Request::METHOD_POST, Request::METHOD_PUT]) || !$role) {
return;
}
$entity->setModifiedBy($this->security->getUser());
if (Request::METHOD_POST === $method) {
$entity->setCreatedBy($this->security->getUser());
}
}
}
<?php
namespace App\Entity;
interface PublishedInfoEntityInterface
{
public function setCreatedAt(\DateTimeInterface $dateTime): PublishedInfoEntityInterface;
public function setModifiedAt(\DateTimeInterface $dateTime): PublishedInfoEntityInterface;
}
и создать подписчика для автоматического заполнения createdAt и modifiedAt вот так
<?php
namespace App\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use App\Entity\PublishedInfoEntityInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class PublishedInfoEntitySubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [KernelEvents::VIEW => ['setDataTime', EventPriorities::PRE_WRITE]];
}
public function setDataTime(ViewEvent $event)
{
$entity = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if (!$entity instanceof PublishedInfoEntityInterface || !in_array($method, [Request::METHOD_POST, Request::METHOD_PUT])) {
return;
}
$entity->setCreatedAt(new \DateTime());
if (Request::METHOD_POST === $method){
$entity->setModifiedAt(new \DateTime());
}
}
}
Наконец, каждый класс, у которого вы хотите иметь это свойство, вы просто расширяете этот класс следующим образом
class User extends Base implements UserInterface
И создайте миграцию,
Другой способ - использовать Doctrine Entity Listener.
class SetUserListener
{
private Security $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function prePersist($obj)
{
if (!is_a($obj, Timer::class) &&
!is_a($obj, DailySummary::class) &&
!is_a($obj, Task::class)
) {
return;
}
if ($this->security->getUser()) {
$obj->setUser($this->security->getUser());
}
}
}
Обязательно подключите Entity Listener к своим сервисам.
App\Doctrine\SetUserListener:
tags: [ doctrine.orm.entity_listener ]
В моем случае я использую пакет Gedmo и аннотацию Blameable. Я создал черту вместо сопоставленного суперкласса, как показано ниже:
<?php
namespace App\ORM\Traits;
use App\Entity\User;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
trait OwnerTrait
{
/**
* @var User|null
* @ORM\ManyToOne(targetEntity = "App\Entity\User")
* @ORM\JoinColumn(name = "created_by", referencedColumnName = "id", nullable=true, onDelete = "SET NULL")
*
* @Gedmo\Blameable(on = "create")
*/
protected ?User $createdBy;
/**
* @var User|null
* @ORM\ManyToOne(targetEntity = "App\Entity\User")
* @ORM\JoinColumn(name = "updated_by", referencedColumnName = "id", nullable=true, onDelete = "SET NULL")
*
* @Gedmo\Blameable(on = "update")
*/
protected ?User $updatedBy;
/**
* Set createdBy
* @param User|null $createdBy
* @return $this
*/
public function setCreatedBy(?User $createdBy)
{
$this->createdBy = $createdBy;
return $this;
}
/**
* Returns the user who create the object
* @return User|null
*/
public function getCreatedBy(): ?User
{
return $this->createdBy;
}
/**
* Set updatedBy
* @param User|null $updatedBy
* @return $this
*/
public function setUpdatedBy(?User $updatedBy)
{
$this->updatedBy = $updatedBy;
return $this;
}
/**
* Returns user who is the last to modify object
* @return User|null
*/
public function getUpdatedBy(): ?User
{
return $this->updatedBy;
}
}
И по сущностям
<?php
class Article
{
use OwnerTrait;
}
Отличный ответ, спасибо. Значит, нет возможности автоматизировать это с помощью сущности или доктрины?