Hey @vlx,
it is absolutely doable with the Laminas API tools. You have to think in resources when handling API endpoints. A resource handles only a single entity / a collection of the same entity.
As you said you want to get a seller and all its tickets in a relational collection. Foloowing this purpose you have two resources here. On the one hand you have a seller resource and on the other hand you have a ticket resource. A seller can have many tickets. Many tickets can have a seller. This is a one to many bidirectional relation.
Relations in your entity
Because of the relation of sellers to tickets I 'd recommend using an ORM system like Doctrine. There you can define such a relation directly in the entity. Think of the following example.
<?php
declare(strict_types=1);
namespace Marcel\V1\Rest\Seller;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Marcel\V1\Rest\Ticket\Ticket;
#[ORM\Entity()]
#[ORM\Table(name: "seller")]
class Seller
{
#[ORM\Id, ORM\Column(name: "id", type: "bigint"), ORM\GeneratedValue()]
protected int $id;
#[ORM\OneToMany(targetEntity: Tickets::class, mappedBy: "seller", cascade: [ "persist", "remove" ])]
protected Collection $tickets;
public function __construct()
{
$this->tickets = new ArrayCollection();
}
// getters and setters follow here
}
That 's our seller entity. As you can see every seller can hold a collection of many tickets. The relation from seller to tickets is defined as a one seller to many tickets relation.
<?php
declare(strict_types=1);
namespace Marcel\V1\Rest\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Marcel\V1\Rest\Seller\Seller;
#[ORM\Entity()]
#[ORM\Table(name: "ticket")]
class Ticket
{
#[ORM\Id, ORM\Column(name: "id", type: "bigint"), ORM\GeneratedValue()]
protected int $id;
#[ORM\ManyToOne(target: Seller::class, inverseBy: "tickets", cascade: [ "persist" ])]
#[ORM\JoinColumn(name: "seller_id", referencedColumnName: "id", nullable: false)]
protected Seller $seller;
// getters and setters go here
}
Above you see the ticket entity. It holds a seller property. This is the owning side. A ticket is always owned by a seller. You can reach the seller always from the ticket itself.
The above shown two entities result in two different REST recources.
{{host}}/seller[/:id]
{{host}}/ticket[/:id]
Relation in action
By now as we know the above shown recources you can request a single seller by id.
{{host}}/seller/1
We know already, that we will get all informations about the seller whose primary key is “1” in the database. Doctrine ORM has a default lazy loading setting on to many relations. Because of the lazy loading you won 't get all the tickets in the response, as long as you do not hydrate them by yourself or set the fetch attribute to eager
. Fetching the to many relation with eager, you will retrieve all tickets for this seller in the REST response.
When not fetching with eager, you possibly create a RPC service. The only task for this service is to response with all tickets by a specific seller. Think of an url like this: {{host}}/seller/:id/tickets
.
In the RPC controller you define an indexAction that fetches all the tickets by the given seller.
<?php
declare(strict_types=1);
namespace Marcel\V1\Rpc\Seller;
use Doctrine\Persistence\ObjectManager;
use DoctrineModule\Persistence\ObjectManagerAwareInterface;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\ApiTools\ContentNegotiation\ViewModel;
class SellerController extends AbstractActionController implements ObjectManagerAwareInterface
{
protected ObjectManager $objectManager;
public function indexAction(): ViewModel
{
$sellerId = $this->params()->fromRoute('id', null);
if ($sellerId === null) {
// error handling because of null primary key (422)
}
$seller = $this->getObjectManager()
->getRepository(Seller:class)
->find($id)
if ($seller === false) {
// error handling because no seller found (404)
}
return new ViewModel([
'payload' => $seller->getTickets(),
]);
}
public function getObjectManager(): ObjectManager
{
return $this->objectManager;
}
public function setObjectManager(ObjectManager $objectManager): void
{
$this->objectManager = $objectManager;
}
}
Conclusion
Sure, this might not be the only way to get relations done. But Doctrine offers you a ready to use object relational mapper that overtakes a lot of work for you. The above shown solution just uses two resources and a one to many relation between the two resources. From my point of view you could not do it any easier. Sure, you can code all that for yourself. But keep in mind thinking in resources. That 's the most important thing when it comes to understanding API endpoints.