Обновить
0
0

Пользователь

Отправить сообщение

Дженерики не завезли пока, это много где сложно сделать, не только в php. Формально можно сделать NetworkCollection и возвращать её.

На уровне ORM есть проверка, иначе это всё просто не соберется, посмотрите на маппинг юзера который я приложил, учитывая,что ORM сама генерирует миграции, то она и есть источник истинны в данном случае. Этапа компиляции нет, но стат. анализ всё более моден) и он покрывает ваш случай

См. выше, и еще пример маппинга сущности

Скрытый текст
<?php

declare(strict_types=1);

namespace App\Auth\Entity\User;

use App\Auth\Service\PasswordHasher;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use DomainException;

#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'auth_users')]
final class User
{
    #[ORM\Column(type: IdType::NAME)]
    #[ORM\Id]
    private Id $id;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
    private DateTimeImmutable $date;

    #[ORM\Column(type: EmailType::NAME, unique: true)]
    private Email $email;

    #[ORM\Column(type: Types::STRING, nullable: true)]
    private ?string $passwordHash = null;

    #[ORM\Column(type: StatusType::NAME, length: 16)]
    private Status $status;

    #[ORM\Embedded(class: Token::class)]
    private ?Token $joinConfirmToken = null;

    #[ORM\Embedded(class: Token::class)]
    private ?Token $passwordResetToken = null;

    #[ORM\Column(type: EmailType::NAME, nullable: true)]
    private ?Email $newEmail = null;

    #[ORM\Embedded(class: Token::class)]
    private ?Token $newEmailToken = null;

    #[ORM\Column(type: RoleType::NAME, length: 16)]
    private Role $role;

    /**
     * @var Collection<int, UserNetwork>
     */
    #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserNetwork::class, cascade: ['all'], orphanRemoval: true)]
    private Collection $networks;

    private function __construct(Id $id, DateTimeImmutable $date, Email $email, Status $status)
    {
        $this->id = $id;
        $this->date = $date;
        $this->email = $email;
        $this->status = $status;
        $this->role = Role::user();
        $this->networks = new ArrayCollection();
    }

    public static function joinByNetwork(
        Id $id,
        DateTimeImmutable $date,
        Email $email,
        Network $network
    ): self {
        $user = new self($id, $date, $email, Status::active());
        $user->networks->add(new UserNetwork($user, $network));
        return $user;
    }

    public static function requestJoinByEmail(
        Id $id,
        DateTimeImmutable $date,
        Email $email,
        string $passwordHash,
        Token $token
    ): self {
        $user = new self($id, $date, $email, Status::wait());
        $user->passwordHash = $passwordHash;
        $user->joinConfirmToken = $token;
        return $user;
    }

    public function confirmJoin(string $token, DateTimeImmutable $date): void
    {
        if ($this->joinConfirmToken === null) {
            throw new DomainException('Confirmation is not required.');
        }
        $this->joinConfirmToken->validate($token, $date);
        $this->activate();
    }

    public function attachNetwork(Network $network): void
    {
        foreach ($this->networks as $existing) {
            if ($existing->getNetwork()->isEqualTo($network)) {
                throw new DomainException('Network is already attached.');
            }
        }
        $this->networks->add(new UserNetwork($this, $network));
        if ($this->isWait()) {
            $this->activate();
        }
    }

    public function requestPasswordReset(Token $token, DateTimeImmutable $date): void
    {
        if (!$this->isActive()) {
            throw new DomainException('User is not active.');
        }
        if ($this->passwordResetToken !== null && !$this->passwordResetToken->isExpiredTo($date)) {
            throw new DomainException('Resetting is already requested.');
        }
        $this->passwordResetToken = $token;
    }

    public function resetPassword(string $token, DateTimeImmutable $date, string $hash): void
    {
        if ($this->passwordResetToken === null) {
            throw new DomainException('Resetting is not requested.');
        }
        $this->passwordResetToken->validate($token, $date);
        $this->passwordResetToken = null;
        $this->passwordHash = $hash;
    }

    public function changePassword(string $current, string $new, PasswordHasher $hasher): void
    {
        if ($this->passwordHash === null) {
            throw new DomainException('User does not have an old password.');
        }
        if (!$hasher->validate($current, $this->passwordHash)) {
            throw new DomainException('Incorrect current password.');
        }
        $this->passwordHash = $hasher->hash($new);
    }

    public function requestEmailChanging(Token $token, DateTimeImmutable $date, Email $email): void
    {
        if (!$this->isActive()) {
            throw new DomainException('User is not active.');
        }
        if ($this->email->isEqualTo($email)) {
            throw new DomainException('Email is already same.');
        }
        if ($this->newEmailToken !== null && !$this->newEmailToken->isExpiredTo($date)) {
            throw new DomainException('Changing is already requested.');
        }
        $this->newEmail = $email;
        $this->newEmailToken = $token;
    }

    public function confirmEmailChanging(string $token, DateTimeImmutable $date): void
    {
        if ($this->newEmail === null || $this->newEmailToken === null) {
            throw new DomainException('Changing is not requested.');
        }
        $this->newEmailToken->validate($token, $date);
        $this->email = $this->newEmail;
        $this->newEmail = null;
        $this->newEmailToken = null;
    }

    public function changeRole(Role $role): void
    {
        $this->role = $role;
    }

    public function remove(): void
    {
        if (!$this->isWait()) {
            throw new DomainException('Unable to remove active user.');
        }
    }

    public function isWait(): bool
    {
        return $this->status->isWait();
    }

    public function isActive(): bool
    {
        return $this->status->isActive();
    }

    public function getId(): Id
    {
        return $this->id;
    }

    public function getDate(): DateTimeImmutable
    {
        return $this->date;
    }

    public function getEmail(): Email
    {
        return $this->email;
    }

    public function getRole(): Role
    {
        return $this->role;
    }

    public function getPasswordHash(): ?string
    {
        return $this->passwordHash;
    }

    public function getJoinConfirmToken(): ?Token
    {
        return $this->joinConfirmToken;
    }

    public function getPasswordResetToken(): ?Token
    {
        return $this->passwordResetToken;
    }

    public function getNewEmail(): ?Email
    {
        return $this->newEmail;
    }

    public function getNewEmailToken(): ?Token
    {
        return $this->newEmailToken;
    }

    /**
     * @return Network[]
     */
    public function getNetworks(): array
    {
        /** @var Network[] */
        return $this->networks->map(static fn (UserNetwork $network) => $network->getNetwork())->toArray();
    }

    #[ORM\PostLoad]
    public function checkEmbeds(): void
    {
        if ($this->joinConfirmToken && $this->joinConfirmToken->isEmpty()) {
            $this->joinConfirmToken = null;
        }
        if ($this->passwordResetToken && $this->passwordResetToken->isEmpty()) {
            $this->passwordResetToken = null;
        }
        if ($this->newEmailToken && $this->newEmailToken->isEmpty()) {
            $this->newEmailToken = null;
        }
    }

    private function activate(): void
    {
        if ($this->isActive()) {
            throw new DomainException('User is already active.');
        }

        $this->status = Status::active();
        $this->joinConfirmToken = null;
    }
}

Типы данных явно указаны в самом первом примере. Может вы не поняли, но класс через аннотации привязан к полям. Да в php нет этапа компиляции, но есть статические анализаторы кода которые позволяют проверять более сложные вещи чем соответствие типа аннотации. Но здесь даже без них будет работать. Вот пример более реального кода

Скрытый текст
<?php

declare(strict_types=1);

namespace App\Auth\Entity\User;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use DomainException;

final readonly class UserRepository
{
    /**
     * @param EntityRepository<User> $repo
     */
    public function __construct(private EntityManagerInterface $em, private EntityRepository $repo) {}

    public function hasByEmail(Email $email): bool
    {
        return $this->repo->createQueryBuilder('t')
            ->select('COUNT(t.id)')
            ->andWhere('t.email = :email')
            ->setParameter(':email', $email->getValue())
            ->getQuery()->getSingleScalarResult() > 0;
    }

    public function hasByNetwork(Network $network): bool
    {
        return $this->repo->createQueryBuilder('t')
            ->select('COUNT(t.id)')
            ->innerJoin('t.networks', 'n')
            ->andWhere('n.network.name = :name and n.network.identity = :identity')
            ->setParameter(':name', $network->getName())
            ->setParameter(':identity', $network->getIdentity())
            ->getQuery()->getSingleScalarResult() > 0;
    }

    public function findByJoinConfirmToken(string $token): ?User
    {
        return $this->repo->findOneBy(['joinConfirmToken.value' => $token]);
    }

    public function findByPasswordResetToken(string $token): ?User
    {
        return $this->repo->findOneBy(['passwordResetToken.value' => $token]);
    }

    public function findByNewEmailToken(string $token): ?User
    {
        return $this->repo->findOneBy(['newEmailToken.value' => $token]);
    }

    public function get(Id $id): User
    {
        $user = $this->repo->find($id->getValue());
        if ($user === null) {
            throw new DomainException('User is not found.');
        }
        return $user;
    }

    public function getByEmail(Email $email): User
    {
        $user = $this->repo->findOneBy(['email' => $email->getValue()]);
        if ($user === null) {
            throw new DomainException('User is not found.');
        }
        return $user;
    }

    public function add(User $user): void
    {
        $this->em->persist($user);
    }

    public function remove(User $user): void
    {
        $this->em->remove($user);
    }
}
// $em instanceof EntityManager
// All users that are 20 years old
$users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20));
// All users that are 20 years old and have a surname of 'Miller'
$users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20, 'surname' => 'Miller'));
// A single user by its nickname
$user = $em->getRepository('MyProject\Domain\User')->findOneBy(array('nickname' => 'romanb'));

2) DQL stands for Doctrine Query Language and is an Object Query Language derivative that is very similar to the Hibernate Query Language (HQL) or the Java Persistence Query Language (JPQL).

Доктрина на DataMapper появилась в 2011 году.

Вот так?)

use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'products')]
class Product
{
    #[ORM\Id]
    #[ORM\Column(type: 'integer')]
    #[ORM\GeneratedValue]
    private int|null $id = null;
    #[ORM\Column(type: 'string')]
    private string $name;
    // .. (other code)
}

Проблемы Bitrix'a оставим Bitrix'у. Кажется вы давно не заглядывали в PHP, там есть declare(strict_types=1) которая появилась в 2015 году) Кстати говорят что DoctrineORM аналог спринга, насколько полный у вас есть возможность оценить по документации

Ответ на оба вопроса - да

Стандарт покрывает много чего, поэтому по большей части приходится следить чтобы случайно чего-нибудь не залетело. Смена базы это больше миф, но поскольку затраты на это не велики, почему бы и нет.

возможность малотрудозатратной смены БД - если схема не менялась и база реляционная, то sql продолжит работать ибо стандарт, а если менялась (схема или база) то затраты будут в любом случае не малые.

Менять поля - часто сразу запрещено, если в проде, значит в камне.

Не могли бы вы развернуть мысль, чем ORM так лучше в компилируемых языках? пару примеров было бы идеально (один тоже сойдет). Я правильно понял что проблема генерации оптимальных запросов никуда не делась в компилируемых языках и часть из них вы пишете вручную?

QueryBuilder это абстракция над SQL, и ей можно пользоваться пока она не становится сложнее, чем то что она абстрагирует, у меня есть запросы которые занимают экран в SQL'е, если конвертнуть это в вызов билдера то это крайне сложно читать, возникает вопрос: зачем? Выше писали что есть хорошие билдеры ну...- нет, абстракция как известно всегда течёт, как только появится билдер который покроет мощность SQL он будет такой же сложный как сам SQL, чтобы им пользоваться нужно будет долго изучать, так же долго как SQL, возникает вопрос: зачем? ответ известен, оно же автоматически смапит результаты, а не проще сначала сделать запрос, а потом смапить результаты? в том числе с помощью гидратора ORM, это гораздо более читаемо. Тут выше писали про сложные запросы внутри ORM, когда я такие пишу (иногда приходится) я физически ощущаю, что занимаюсь ерундой, попыткой выразить через абстракцию то для чего она не предназначена, свои простые 80 процентов она выполняет хорошо, но те 20 всегда будут за чистым SQL.

Спасибо за ответ. Если дто делается из массива, то метод его создания кладется рядом, да если в другом месте будет точно такой же массив - будет неудобно) Но часто у Вас встречается fromArray из двух разных мест? у меня - никогда (тут стоит уточнить что дто я создаю относительно часто вот только для этого всегда лучше подходит конструктор, а чтобы применить fromArray массив должен быть прям один в один), хотя это может сильно зависеть от проекта... Использовать же fromFile т.е. делать парсинг файла внутри дто (я правильно понял?) мне не позволит совесть (по мне это нарушение srp)

На самом деле если порезать Services на методы, то получатся Actions, а если порезать Repositories на методы получатся Queries. Action (пишет в базу) может вызывать другие Actions и Queries для выполнения своей бизнес задачи. Query (читает из базы) может вызывать только другие Query, Поскольку action может вызывать другой action может возникнуть кольцевая зависимость с чем и борется создатель упомянутого Porto. Но на практике на мелких проектах это должно ловится на деве и тестах и усложнять описанную схему я не хочу, Проблема в моём подходе пока одна - слишком много мелких классов т..к на getById нужен отдельный класс и мне придется объединять их в папки, которые по сути и будут репозиториеми, а папок и так уже много с учетом модулей, дто и прочего.

Кстати, всегда было интересно, почему все делают вот так

UserDTO::fromRequest($request)

вместо того чтобы делать вот так

$request->getUserDto()

ведь реквест уже содержит все данные и валидацию для них, а получение из него DTO уникально и больше нигде кроме места с реквестом не пригодится, так почему метод формирования у находится в DTO?

Согласен с Вами, моё личное мнение, что весь важный бизнес код должен быть внутри useCase'а , поэтому письмо еще можно вынести в события, но передачу в службу доставки - нет. Иначе получится лазанья код из разбросанной по приложению важной логики, которую устанешь собирать. Составление карты "этого", вместо того чтобы так не делать, больше похоже на способ создать себе сложности, а потом героически их преодолевать.

Если Вы сомневаетесь нужен ли Вам фреймворк, то он Вам точно нужен, т.к. у Вас не хватает экспертизы в вопросе, иначе бы сомнений не было. Даже если Вы уверены что фреймворк не нужен это может быть ошибкой. На мой взгляд этим вопросом чаще всего задаются новички, для них ответ однозначный: «нужен» и нет это не означает что нужно «программировать на фреймворке» т.е. изучать только фреймворк, и не изучать возможности языка программирования. Попробовать написать свой фреймворк полезно, начинаешь понимать почему те или иные решение были взяты в известных фреймворках, но делать такие эксперименты нужно на «второстепенных» проектах, которые переписать или выкинуть не составит большого труда.
На локальной машине нормально работает, в инетах полно видео (wsl 2 vcxsrv), не натив конечно, но и секунды ждать не приходится. Проверял на Firefox, PHPstorm. В конце года обещают поддержку из коробки для запуска gui из wsl, возможно будет еще лучше.
Если использовать git внутри wsl, то PhpStorm не подсвечивает строки с изменениями (это логично), если использовать git в Windows, то в какой-то момент перестают трекаться изменения, в диалоге коммита нужно нажимать «обновить», сделайте хоть чтобы «обновить» вызывалось автоматически при открытии диалога коммита (это ведь ничего не поломает), а то так можно «удачно» пропустить важные файлы. (проект лежит в папке пользователя в wsl)
Пожалуйста добавьте иерархическую организацию для варианта когда вкладки сбоку ( как в Tree Style Tab аддоне для Firefox).
Меня одного удивили 22" мониторы в почти 2018-том? Почему? Ну и кроме мониторов, есть же
DNK-H

Информация

В рейтинге
5 135-й
Зарегистрирован
Активность