Используя платформу Api, автоматически назначьте пользователя объекту (OneToMany)

Вот мой сценарий:

У меня есть сущность 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;
    }


}
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
8
0
4 014
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

Это должно быть возможно при использовании 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);

    }
}

Отличный ответ, спасибо. Значит, нет возможности автоматизировать это с помощью сущности или доктрины?

Wildcard27 27.09.2018 06:21

Другой способ - atlantic18.github.io/DoctrineExtensions/doc/blameable.html, но я рекомендую явный способ, как указано выше. Слушатели доктрины, как правило, запутываются, я всегда стараюсь избегать их и сам моделирую ограничения предметной области. Также облегчает тестирование, так как вы можете тестировать поведение без сохранения.

mblaettermann 27.09.2018 06:44

@ Wildcard27, нет способа автоматизировать это. не имеет ничего общего с платформой api как таковой. объект не должен зависеть от контейнера службы, но вы должны установить данные (в данном случае пользователя) извне. плюс: ваше требовательное отношение звучит для меня немного недружелюбно в таких проектах с открытым исходным кодом, как платформа api.

LBA 27.09.2018 14:56

@ Wildcard27 Принять мой ответ как решение проблемы?

mblaettermann 27.09.2018 17:34

@mblaettermann Я как раз реализую это сейчас. Извините за задержку. После тестирования я прокомментирую или помечу как принятый

Wildcard27 28.09.2018 05:24

@LBA Спасибо за комментарий, я перефразировал вопрос. Я вовсе не собирался показаться требовательным. Я ценю то, что люди уделяют время тому, чтобы помочь.

Wildcard27 28.09.2018 05:28

Обратите внимание, что начиная с Symfony 4.3 GetResponseForControllerResultEvent был переименован в ViewEvent

Guaycuru 25.04.2020 19:01

Вы можете создать объект с именем 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;
    }
}

Создайте класс подписчика для автоматического создания createdBy и modifiedBy, как в этом коде

<?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());
         }
    }
}

Если вы хотите добавить автоматические createdAt и modifiedAt, вы должны создать класс интерфейса с именем PublishedInfoEntityInterface для createdAt и modifiedAt, и этот класс напишите этот код:

<?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;
}

Другие вопросы по теме