Coder Social home page Coder Social logo

mtarld / apip-ddd Goto Github PK

View Code? Open in Web Editor NEW
293.0 20.0 49.0 328 KB

An example of hexagonal API Platform 3 implementation

License: MIT License

Shell 1.83% PHP 93.13% Twig 0.53% Dockerfile 2.33% Makefile 2.17%
api-platform ddd hexagonal-architecture example

apip-ddd's People

Contributors

alanpoulain avatar chalasr avatar cpetit-sigma-fr avatar juuuuuu avatar ker0x avatar mstanowski avatar mtarld avatar soyuka avatar spomky avatar wazum avatar yceruto avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

apip-ddd's Issues

Doctrine et Vendor dans le Domain ?

Bonjour à tous,

Je viens de voir votre conférence sur Youtube, laquelle ma beaucoup apporté.
Par contre je ne comprends pas pourquoi vous avez mis Doctrine dans le Domaine, alors que dans la conférence vous dite qu'il ne faut pas faire ça.
Pouvez-vous m'expliquer votre changement de position ?

Merci pour le partage !

Stuck make install

Hi there!
Cloned this repo, I run make install but...
image

What's wrong?
I cloned this repo today.

Wrong count with DoctrineRepository

In some cases (no paginator and queries with joins, etc.), the count method of the DoctrineRepository seems to return wrong results :

if (null !== $paginator = $this->paginator()) {
return count($paginator);
}
return (int) (clone $this->queryBuilder)
->select('count(1)')
->getQuery()
->getSingleScalarResult();

I had better results relying on the doctrine paginator's logic :

if (null !== $paginator = $this->paginator()) {
    return count($paginator);
}

return (new Paginator(clone $this->queryBuilder))->count();

Typo in shared services definition for kernel path

->exclude([__DIR__.'/../../src/Shared/Infrastructure/Kernel.php']);

If I'm right, the Kernel path is src/Shared/Infrastructure/Symfony/Kernel.php.

I was a bit curious though... What I understood by debugging with the debug:container is that this typo has no effect on the services list in the container in the end since Symfony seems to override the definition for the kernel class anyway (with the "kernel" alias).

To me, it looks like this exclude (present in the default config of a strandard Symfony project) just improves performance a bit.

Doctrine and Vendor in the Domain?

I just saw your conference on Youtube, which helped me a lot.
On the other hand, I don't understand why you put Doctrine in the Domain, when in the conference you said that you shouldn't do that.
Can you explain your change of position to me?

Thank you for sharing !

Add async commands

Hi, I am using this repository as base for my new project. However this repo does not implement async commands and as a consequence it does not show how to consider async commands inside a command bus implementation.

I rewritten the MessengerCommandBus implementation however this does not satisfy PHPStan as returning null for async commands results in error

Method MessengerCommandBus::dispatch() should return T but returns null.                                
💡 Type null is not always the same as T. It breaks the contract for some argument types, typically subtypes.
final class MessengerCommandBus implements CommandBusInterface
{
    public function __construct(MessageBusInterface $commandBus)
    {
        $this->messageBus = $commandBus;
    }

    /**
     * @template T
     *
     * @param CommandInterface<T> $command
     *
     * @return T
     */
    public function dispatch(CommandInterface $command): mixed
    {
        try {
            $envelope = $this->messageBus->dispatch($message);
            /** @var HandledStamp[] $handledStamps */
            $handledStamps = $envelope->all(HandledStamp::class);

            if (!$handledStamps) {
                // async command
                return null;
            }
            
            if (\count($handledStamps) > 1) {
                $handlers = implode(', ', array_map(fn (HandledStamp $stamp): string => sprintf('"%s"', $stamp->getHandlerName()), $handledStamps));

                throw new LogicException(sprintf('Message of type "%s" was handled multiple times. Only one handler is expected when using "%s::%s()", got %d: %s.', get_debug_type($envelope->getMessage()), static::class, __FUNCTION__, \count($handledStamps), $handlers));
            }

            return $handledStamps[0]->getResult();
        } catch (HandlerFailedException $e) {
            if ($exception = current($e->getWrappedExceptions())) {
                throw $exception;
            }

            throw $e;
        }
    }
}

Question about hydra context

In the docs for openApicontext, in the schema section it appears Book.AnonymizeBooksCommand.jsonld and Book.DiscountBookPayload.jsonld which are the inputs for some of custom methods for the Book resource. But in hydra context, there's no definition or mention in the docs.jsonld
I have added some hydraContext for the custom methods, and for discount and anonymize end point, in the expects section of the hydraContext I have DiscountBookCommand::class or AnonymizeBooksCommand::class which will result in docs.jsonld in "expects": "App\Application\Library\Command\AnonymizeBooksCommand" or "expects": "App\Application\Library\Command\DiscountBookCommand". If Book.AnonymizeBooksCommand.jsonld and Book.DiscountBookPayload.jsonld were to exists in the docs.jsonld as class definition then it should reference the expects filed.

Best practices to work with relations in Api Platform 3 and DDD

I have two entities in a one-to-many relation.
To refer to your example of the BookStore imagine that a Book has a Category in a one-to-many relation. A Category is in relation with many Book entities.
I have created a sort of "copy" of the book store for the folders and files structure for both my two entities, with create commands, other commands, item and collection provider, repositories, resources, etc etc. Same as the book store.
My problem is in the creation of a Book by BookResource and the various commands and command handlers when I have to save the related Category at the same time. Let me explain with the code.

The BookResource has the POST operation defined like this:

new Post(
    validationContext: ['groups' => ['create']],
    processor: CreateBookProcessor::class,
),

Your BookResource class is the seguent:

final class BookResource
{
    public function __construct(
        #[ApiProperty(identifier: true, readable: false, writable: false)]
        public ?AbstractUid $id = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 255, groups: ['create', 'Default'])]
        public ?string $name = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 1023, groups: ['create', 'Default'])]
        public ?string $description = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 255, groups: ['create', 'Default'])]
        public ?string $author = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 65535, groups: ['create', 'Default'])]
        public ?string $content = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\PositiveOrZero(groups: ['create', 'Default'])]
        public ?int $price = null,
    ) {
    }

    public static function fromModel(Book $book): static
    {
        return new self(
            $book->id->value,
            $book->name->value,
            $book->description->value,
            $book->author->value,
            $book->content->value,
            $book->price->amount,
        );
    }
}

Now, how can I define a new property to associate the category to the book? Which is the type that I must assign to the category property?
My question is about what I see in the swagger and in which way I define and use the property in the command and command handler.
If I define category only as the category id, for example a string for uuid, the swagger show me a thing like this in the request body:

"category": "string",

If I accept the category as a string, for example the uuid, then I receive the uuid in the $data in your CreateBookProcessor.

final class CreateBookProcessor implements ProcessorInterface
{
    public function __construct(
        private CommandBusInterface $commandBus,
    ) {
    }

    /**
     * @param mixed $data
     *
     * @return BookResource
     */
    public function process($data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        Assert::isInstanceOf($data, BookResource::class);

        Assert::notNull($data->name);
        Assert::notNull($data->description);
        Assert::notNull($data->author);
        Assert::notNull($data->content);
        Assert::notNull($data->price);
        Assert::notNull($data->category); // The category received

        $command = new CreateBookCommand(
            new BookName($data->name),
            new BookDescription($data->description),
            new Author($data->author),
            new BookContent($data->content),
            new Price($data->price),
        );

        /** @var Book $model */
        $model = $this->commandBus->dispatch($command);

        return BookResource::fromModel($model);
    }
}

The processor make a new CreateBookCommand to dispatch to the command bus. The command is defined like this.

final class CreateBookCommand implements CommandInterface
{
    public function __construct(
        public readonly BookName $name,
        public readonly BookDescription $description,
        public readonly Author $author,
        public readonly BookContent $content,
        public readonly Price $price,
    ) {
    }
}

The CreateBookCommandHandler is defined like this:

final class CreateBookCommandHandler implements CommandHandlerInterface
{
    public function __construct(private BookRepositoryInterface $bookRepository)
    {
    }

    public function __invoke(CreateBookCommand $command): Book
    {
        $book = new Book(
            $command->name,
            $command->description,
            $command->author,
            $command->content,
            $command->price,
        );

        $this->bookRepository->add($book);

        return $book;
    }
}

To use the category id received in the processor I think that I have to add in the create command a $category property of type CategoryId that is the category id value object like this:

final class CreateBookCommand implements CommandInterface
{
    public function __construct(
        public readonly BookName $name,
        public readonly BookDescription $description,
        public readonly Author $author,
        public readonly BookContent $content,
        public readonly Price $price,
        public readonly CategoryId $category, // The category property
    ) {
    }
}

If I add the category like this, I have to initialize the command like that in the processor.

        $command = new CreateBookCommand(
            new BookName($data->name),
            new BookDescription($data->description),
            new Author($data->author),
            new BookContent($data->content),
            new Price($data->price),
            new CategoryId(Uuid::fromString($data->category)) // The category
        );

The command handler is now a big problem because I don't know in which way I can save the category to the new book because the Book entity has the Category public property and not a string like the uuid received in the handler.
My actual solution is to find the category with the provided id by the category reporitory and set the found object in the Book entity. The use the book repository to save the new book.

final class CreateBookCommandHandler implements CommandHandlerInterface
{
    public function __construct(private BookRepositoryInterface $bookRepository)
    {
    }

    public function __invoke(CreateBookCommand $command): Book
    {
        $book = new Book(
            $command->name,
            $command->description,
            $command->author,
            $command->content,
            $command->price,
        );

        $foundCategory = $this->courseRepository->ofId($command->categoryId);
        if (is_null($foundCategory)) {
            throw new NotFoundHttpException("Category not found by id " . $command->categoryId->value->toRfc4122());
        }
        
        $book->category = $foundCategory;

        $this->bookRepository->add($book);

        return $book;
    }
}

With this code I can save correctly the category to the book but with the category defined like this in all the classes I have my big problem when getting the data. Like the processor, the state provider uses the fromModel method of the BookResource. The method return a new self object and this is the problem because I defined the category as a string and in the swagger result I only can send a string and nothing else, like the Category object for example.

How can I accept a string in the category property in the book resource and send back the IRI of the associated Category when I retrieve a book item or books collection?

If I define the category property in the book resource as a Category object, and then in all the other classes, I see all the Category properties in the POST request body and I don't want to create a new category when I create a new Book. I only want to create the new book and associate the category to it! This last solution send in output the whole category in the book single item but I don't want this solution because it's not the IRI at the same way of the first solution!

If it's not clear ask me!

Repository pattern

This is not really an issue, but more an information request. I really like the way repositories are implemented in this project. A problem I usually have with repositories is their size when implementing all the queries needed for the model.

From what I see here I understand that the basic idea is to treat them like immutable collections where methods are basically filters used to reduce the size of the collection.

Can you point me to a book, blog post or another resource that explore the subject in detail?

There's only a little inconsistency though between the doctrine book repository and the in memory one:

  • in the former if you filter and then you call ofId(), doctrine searches the id in the entire table
  • in the latter if you do the same you just search in the filtered records set

Except this the idea is excellent.

Questions on best practice example

Thanks for the example and the presentation, but as this should be an example project for best practices(?), allow me to ask some questions:

  1. UpdateBookCommandHandler does not update

But remove the complete book and then add it again.

$this->bookRepository->remove($book);
$this->bookRepository->add($book);

Why would you do that?

  1. Properties on the domain model are public

And despite telling me in the Chat on the API platform conference that they are readonly, they are not.
You even use this fact in the above-mentioned command handler:

$book->name = $command->name ?? $book->name;
$book->description = $command->description ?? $book->description;
$book->author = $command->author ?? $book->author;
$book->content = $command->content ?? $book->content;

which contradicts ideas DDD stands for. You don't want to modify single (public) properties and expose the object to the risk of entering an invalid state!
The correct way to do this update is through a single public method that updates all (private/protected) properties at once (and checks some rules).

Error during parsing: unrecognized protocol option 'experimental_http3'

Docker/Compose Versions:

$ docker-compose --version                                
docker-compose version 1.29.2, build unknown

docker --version        
Docker version 20.10.19+dfsg1, build d85ef84

Docker Compose Config:

# docker-compose config | yq
services:
  caddy:
    build:
      context: /home/qualeo/Downloads/apip-ddd
      target: symfony_caddy
    depends_on:
      php:
        condition: service_started
    environment:
      MERCURE_PUBLISHER_JWT_KEY: '!ChangeMe!'
      MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeMe!'
      SERVER_NAME: localhost, caddy:80
    ports:
      - protocol: tcp
        published: 80
        target: 80
      - protocol: tcp
        published: 443
        target: 443
      - protocol: udp
        published: 443
        target: 443
    restart: unless-stopped
    volumes:
      - caddy_config:/config:rw
      - caddy_data:/data:rw
      - /home/qualeo/Downloads/apip-ddd/docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - /home/qualeo/Downloads/apip-ddd/public:/srv/app/public:ro
      - php_socket:/var/run/php:rw
  database:
    environment:
      POSTGRES_DB: app
      POSTGRES_PASSWORD: ChangeMe
      POSTGRES_USER: symfony
    image: postgres:13-alpine
    ports:
      - target: 5432
    volumes:
      - db-data:/var/lib/postgresql/data:rw
  php:
    build:
      args:
        SKELETON: symfony/skeleton
        STABILITY: stable
        SYMFONY_VERSION: ''
      context: /home/qualeo/Downloads/apip-ddd
      target: symfony_php
    environment:
      APP_ENV: dev
      MERCURE_JWT_SECRET: '!ChangeMe!'
      MERCURE_PUBLIC_URL: https://localhost/.well-known/mercure
      MERCURE_URL: http://caddy/.well-known/mercure
    healthcheck:
      interval: 10s
      retries: 3
      start_period: 30s
      timeout: 3s
    restart: unless-stopped
    volumes:
      - /home/qualeo/Downloads/apip-ddd:/srv/app:rw,cached
      - /srv/app/var
      - /home/qualeo/Downloads/apip-ddd/docker/php/conf.d/symfony.dev.ini:/usr/local/etc/php/conf.d/symfony.ini:rw
      - php_socket:/var/run/php:rw
version: '3.4'
volumes:
  caddy_config: {}
  caddy_data: {}
  db-data: {}
  php_socket: {}

Steps to Reproduce:

git clone https://github.com/mtarld/apip-ddd.git
cd apip-ddd
make install
# ...
  - Downloading dnoegel/php-xdg-base-dir (v0.1.1)
  - Downloading amphp/amp (v2.6.2)
  - Downloading amphp/byte-stream (v1.8.1)
  - Downloading vimeo/psalm (4.27.0)
  8/76 [==>-------------------------]  10%make[1]: *** [Makefile:52: vendor] Error 137
make: *** [Makefile:47: install] Error 2

make install
# ...
Executing script cache:clear [OK]
Executing script assets:install public [OK]

Dropped database "app" for connection named default
Created database "app" for connection named default

 Updating database schema...

     4 queries were executed

                                                                                                                        
 [OK] Database schema updated successfully!

make stop
# ...

make start
Creating apip-ddd_php_1      ... done
Creating apip-ddd_database_1 ... done
Creating apip-ddd_caddy_1    ... done

docker-compose logs -f caddy
Attaching to apip-ddd_caddy_1
# ...
caddy_1     | Error: adapting config using caddyfile: parsing caddyfile tokens for 'servers': /etc/caddy/Caddyfile:7 - Error during parsing: unrecognized protocol option 'experimental_http3'
caddy_1     | {"level":"info","ts":1667602742.0740893,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
caddy_1     | {"level":"warn","ts":1667602742.074148,"logger":"caddyfile","msg":"DEPRECATED: protocol sub-option will be removed soon"}
caddy_1     | Error: adapting config using caddyfile: parsing caddyfile tokens for 'servers': /etc/caddy/Caddyfile:7 - Error during parsing: unrecognized protocol option 'experimental_http3'

$ docker-compose ps caddy 
      Name                    Command                 State      Ports
----------------------------------------------------------------------
apip-ddd_caddy_1   caddy run --config /etc/ca ...   Restarting  

make stop

Cannot initialize readonly property AggregateRootId:: :$value from scope BookId

Hey,

I followed the instructions in the readme to start the project as I tried the endpoints on the /api page I received the following error:

Commit 635d064
Url: https://localhost/api/books?page=1

JSON Response ```json { "@context": "/api/contexts/Error", "@type": "hydra:Error", "hydra:title": "An error occurred", "hydra:description": "Cannot initialize readonly property App\\Shared\\Domain\\ValueObject\\AggregateRootId::$value from scope App\\BookStore\\Domain\\ValueObject\\BookId", "trace": [ { "namespace": "", "short_class": "", "class": "", "type": "", "function": "", "file": "/srv/app/vendor/doctrine/persistence/src/Persistence/Reflection/RuntimePublicReflectionProperty.php", "line": 59, "args": [] }, { "namespace": "", "short_class": "ReflectionProperty", "class": "ReflectionProperty", "type": "->", "function": "setValue", "file": "/srv/app/vendor/doctrine/persistence/src/Persistence/Reflection/RuntimePublicReflectionProperty.php", "line": 59, "args": [ [ "object", "App\\BookStore\\Domain\\ValueObject\\BookId" ], [ "object", "Symfony\\Component\\Uid\\UuidV4" ] ] }, { "namespace": "Doctrine\\Persistence\\Reflection", "short_class": "RuntimePublicReflectionProperty", "class": "Doctrine\\Persistence\\Reflection\\RuntimePublicReflectionProperty", "type": "->", "function": "setValue", "file": "/srv/app/vendor/doctrine/persistence/src/Persistence/Reflection/TypedNoDefaultReflectionPropertyBase.php", "line": 64, "args": [ [ "object", "App\\BookStore\\Domain\\ValueObject\\BookId" ], [ "object", "Symfony\\Component\\Uid\\UuidV4" ] ] }, { "namespace": "Doctrine\\Persistence\\Reflection", "short_class": "TypedNoDefaultRuntimePublicReflectionProperty", "class": "Doctrine\\Persistence\\Reflection\\TypedNoDefaultRuntimePublicReflectionProperty", "type": "->", "function": "setValue", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php", "line": 40, "args": [ [ "object", "App\\BookStore\\Domain\\ValueObject\\BookId" ], [ "object", "Symfony\\Component\\Uid\\UuidV4" ] ] }, { "namespace": "Doctrine\\ORM\\Mapping", "short_class": "ReflectionReadonlyProperty", "class": "Doctrine\\ORM\\Mapping\\ReflectionReadonlyProperty", "type": "->", "function": "setValue", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php", "line": 81, "args": [ [ "object", "App\\BookStore\\Domain\\ValueObject\\BookId" ], [ "object", "Symfony\\Component\\Uid\\UuidV4" ] ] }, { "namespace": "Doctrine\\ORM\\Mapping", "short_class": "ReflectionEmbeddedProperty", "class": "Doctrine\\ORM\\Mapping\\ReflectionEmbeddedProperty", "type": "->", "function": "setValue", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php", "line": 2753, "args": [ [ "object", "App\\BookStore\\Domain\\Model\\Book" ], [ "object", "Symfony\\Component\\Uid\\UuidV4" ] ] }, { "namespace": "Doctrine\\ORM", "short_class": "UnitOfWork", "class": "Doctrine\\ORM\\UnitOfWork", "type": "->", "function": "createEntity", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php", "line": 266, "args": [ [ "string", "App\\BookStore\\Domain\\Model\\Book" ], [ "array", { "id.value": [ "object", "Symfony\\Component\\Uid\\UuidV4" ], "name.value": [ "string", "foo" ], "description.value": [ "string", "bar" ], "author.value": [ "string", "jj" ], "content.value": [ "string", "this is an example" ], "price.amount": [ "integer", 10 ] } ], [ "array", { "doctrine.customTreeWalkers": [ "array", [ [ "string", "Doctrine\\ORM\\Tools\\Pagination\\WhereInWalker" ] ] ], "doctrine.id.count": [ "integer", 2 ], "deferEagerLoad": [ "boolean", true ], "fetchAlias": [ "string", "book" ] } ] ] }, { "namespace": "Doctrine\\ORM\\Internal\\Hydration", "short_class": "ObjectHydrator", "class": "Doctrine\\ORM\\Internal\\Hydration\\ObjectHydrator", "type": "->", "function": "getEntity", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php", "line": 492, "args": [ [ "array", { "id.value": [ "object", "Symfony\\Component\\Uid\\UuidV4" ], "name.value": [ "string", "foo" ], "description.value": [ "string", "bar" ], "author.value": [ "string", "jj" ], "content.value": [ "string", "this is an example" ], "price.amount": [ "integer", 10 ] } ], [ "string", "book" ] ] }, { "namespace": "Doctrine\\ORM\\Internal\\Hydration", "short_class": "ObjectHydrator", "class": "Doctrine\\ORM\\Internal\\Hydration\\ObjectHydrator", "type": "->", "function": "hydrateRowData", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php", "line": 148, "args": [ [ "array", { "id_0": [ "string", "6f6413db-3f20-445b-8ef5-30b081fd0f46" ], "name_1": [ "string", "foo" ], "description_2": [ "string", "bar" ], "author_3": [ "string", "jj" ], "content_4": [ "string", "this is an example" ], "price_5": [ "integer", 10 ] } ], [ "array", [] ] ] }, { "namespace": "Doctrine\\ORM\\Internal\\Hydration", "short_class": "ObjectHydrator", "class": "Doctrine\\ORM\\Internal\\Hydration\\ObjectHydrator", "type": "->", "function": "hydrateAllData", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php", "line": 270, "args": [] }, { "namespace": "Doctrine\\ORM\\Internal\\Hydration", "short_class": "AbstractHydrator", "class": "Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator", "type": "->", "function": "hydrateAll", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php", "line": 1234, "args": [ [ "object", "Doctrine\\DBAL\\Result" ], [ "object", "Doctrine\\ORM\\Query\\ResultSetMapping" ], [ "array", { "doctrine.customTreeWalkers": [ "array", [ [ "string", "Doctrine\\ORM\\Tools\\Pagination\\WhereInWalker" ] ] ], "doctrine.id.count": [ "integer", 2 ] } ] ] }, { "namespace": "Doctrine\\ORM", "short_class": "AbstractQuery", "class": "Doctrine\\ORM\\AbstractQuery", "type": "->", "function": "executeIgnoreQueryCache", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php", "line": 1175, "args": [ [ "null", null ], [ "integer", 1 ] ] }, { "namespace": "Doctrine\\ORM", "short_class": "AbstractQuery", "class": "Doctrine\\ORM\\AbstractQuery", "type": "->", "function": "execute", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php", "line": 911, "args": [ [ "null", null ], [ "integer", 1 ] ] }, { "namespace": "Doctrine\\ORM", "short_class": "AbstractQuery", "class": "Doctrine\\ORM\\AbstractQuery", "type": "->", "function": "getResult", "file": "/srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/Tools/Pagination/Paginator.php", "line": 165, "args": [ [ "integer", 1 ] ] }, { "namespace": "Doctrine\\ORM\\Tools\\Pagination", "short_class": "Paginator", "class": "Doctrine\\ORM\\Tools\\Pagination\\Paginator", "type": "->", "function": "getIterator", "file": "/srv/app/src/Shared/Infrastructure/Doctrine/DoctrinePaginator.php", "line": 78, "args": [] }, { "namespace": "App\\Shared\\Infrastructure\\Doctrine", "short_class": "DoctrinePaginator", "class": "App\\Shared\\Infrastructure\\Doctrine\\DoctrinePaginator", "type": "->", "function": "getIterator", "file": "/srv/app/src/Shared/Infrastructure/Doctrine/DoctrineRepository.php", "line": 39, "args": [] }, { "namespace": "App\\Shared\\Infrastructure\\Doctrine", "short_class": "DoctrineRepository", "class": "App\\Shared\\Infrastructure\\Doctrine\\DoctrineRepository", "type": "->", "function": "getIterator", "file": "/srv/app/src/BookStore/Infrastructure/ApiPlatform/State/Provider/BookCollectionProvider.php", "line": 43, "args": [] }, { "namespace": "App\\BookStore\\Infrastructure\\ApiPlatform\\State\\Provider", "short_class": "BookCollectionProvider", "class": "App\\BookStore\\Infrastructure\\ApiPlatform\\State\\Provider\\BookCollectionProvider", "type": "->", "function": "provide", "file": "/srv/app/vendor/api-platform/core/src/State/CallableProvider.php", "line": 43, "args": [ [ "object", "ApiPlatform\\Metadata\\GetCollection" ], [ "array", [] ], [ "array", { "operation": [ "object", "ApiPlatform\\Metadata\\GetCollection" ], "filters": [ "array", { "page": [ "string", "1" ] } ], "operation_name": [ "string", "_api_/books.{_format}_get_collection" ], "resource_class": [ "string", "App\\BookStore\\Infrastructure\\ApiPlatform\\Resource\\BookResource" ], "skip_null_values": [ "boolean", true ], "iri_only": [ "boolean", false ], "request_uri": [ "string", "/api/books?page=1" ], "uri": [ "string", "https://localhost/api/books?page=1" ], "input": [ "null", null ], "output": [ "null", null ] } ] ] }, { "namespace": "ApiPlatform\\State", "short_class": "CallableProvider", "class": "ApiPlatform\\State\\CallableProvider", "type": "->", "function": "provide", "file": "/srv/app/vendor/api-platform/core/src/Symfony/EventListener/ReadListener.php", "line": 88, "args": [ [ "object", "ApiPlatform\\Metadata\\GetCollection" ], [ "array", [] ], [ "array", { "operation": [ "object", "ApiPlatform\\Metadata\\GetCollection" ], "filters": [ "array", { "page": [ "string", "1" ] } ], "operation_name": [ "string", "_api_/books.{_format}_get_collection" ], "resource_class": [ "string", "App\\BookStore\\Infrastructure\\ApiPlatform\\Resource\\BookResource" ], "skip_null_values": [ "boolean", true ], "iri_only": [ "boolean", false ], "request_uri": [ "string", "/api/books?page=1" ], "uri": [ "string", "https://localhost/api/books?page=1" ], "input": [ "null", null ], "output": [ "null", null ] } ] ] }, { "namespace": "ApiPlatform\\Symfony\\EventListener", "short_class": "ReadListener", "class": "ApiPlatform\\Symfony\\EventListener\\ReadListener", "type": "->", "function": "onKernelRequest", "file": "/srv/app/vendor/symfony/event-dispatcher/Debug/WrappedListener.php", "line": 115, "args": [ [ "object", "Symfony\\Component\\HttpKernel\\Event\\RequestEvent" ], [ "string", "kernel.request" ], [ "object", "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher" ] ] }, { "namespace": "Symfony\\Component\\EventDispatcher\\Debug", "short_class": "WrappedListener", "class": "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener", "type": "->", "function": "__invoke", "file": "/srv/app/vendor/symfony/event-dispatcher/EventDispatcher.php", "line": 230, "args": [ [ "object", "Symfony\\Component\\HttpKernel\\Event\\RequestEvent" ], [ "string", "kernel.request" ], [ "object", "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher" ] ] }, { "namespace": "Symfony\\Component\\EventDispatcher", "short_class": "EventDispatcher", "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher", "type": "->", "function": "callListeners", "file": "/srv/app/vendor/symfony/event-dispatcher/EventDispatcher.php", "line": 59, "args": [ [ "array", [ [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ], [ "object", "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener" ] ] ], [ "string", "kernel.request" ], [ "object", "Symfony\\Component\\HttpKernel\\Event\\RequestEvent" ] ] }, { "namespace": "Symfony\\Component\\EventDispatcher", "short_class": "EventDispatcher", "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher", "type": "->", "function": "dispatch", "file": "/srv/app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php", "line": 153, "args": [ [ "object", "Symfony\\Component\\HttpKernel\\Event\\RequestEvent" ], [ "string", "kernel.request" ] ] }, { "namespace": "Symfony\\Component\\EventDispatcher\\Debug", "short_class": "TraceableEventDispatcher", "class": "Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher", "type": "->", "function": "dispatch", "file": "/srv/app/vendor/symfony/http-kernel/HttpKernel.php", "line": 129, "args": [ [ "object", "Symfony\\Component\\HttpKernel\\Event\\RequestEvent" ], [ "string", "kernel.request" ] ] }, { "namespace": "Symfony\\Component\\HttpKernel", "short_class": "HttpKernel", "class": "Symfony\\Component\\HttpKernel\\HttpKernel", "type": "->", "function": "handleRaw", "file": "/srv/app/vendor/symfony/http-kernel/HttpKernel.php", "line": 75, "args": [ [ "object", "Symfony\\Component\\HttpFoundation\\Request" ], [ "integer", 1 ] ] }, { "namespace": "Symfony\\Component\\HttpKernel", "short_class": "HttpKernel", "class": "Symfony\\Component\\HttpKernel\\HttpKernel", "type": "->", "function": "handle", "file": "/srv/app/vendor/symfony/http-kernel/Kernel.php", "line": 202, "args": [ [ "object", "Symfony\\Component\\HttpFoundation\\Request" ], [ "integer", 1 ], [ "boolean", true ] ] }, { "namespace": "Symfony\\Component\\HttpKernel", "short_class": "Kernel", "class": "Symfony\\Component\\HttpKernel\\Kernel", "type": "->", "function": "handle", "file": "/srv/app/vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php", "line": 35, "args": [ [ "object", "Symfony\\Component\\HttpFoundation\\Request" ] ] }, { "namespace": "Symfony\\Component\\Runtime\\Runner\\Symfony", "short_class": "HttpKernelRunner", "class": "Symfony\\Component\\Runtime\\Runner\\Symfony\\HttpKernelRunner", "type": "->", "function": "run", "file": "/srv/app/vendor/autoload_runtime.php", "line": 29, "args": [] }, { "namespace": "", "short_class": "", "class": "", "type": "", "function": "require_once", "file": "/srv/app/public/index.php", "line": 7, "args": [ [ "string", "/srv/app/vendor/autoload_runtime.php" ] ] } ] } ```

Not sure how to fix that issue. readonly properties are also new to me, especially in inheritance.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.