I am working on a symfony/api platform app that allows users to track sports matches. My entities look like this (shortened for brevity):
User.php
class User implements UserInterface
{
// ...
/**
* @ORM\OneToMany(targetEntity=MatchPlayer::class, mappedBy="user")
*/
private $matches;
// ...
}
MatchPlayer.php
class MatchPlayer
{
// ...
/**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="matches")
* @ORM\JoinColumn(onDelete="SET NULL")
*/
private $user;
/**
* @ORM\ManyToOne(targetEntity=Match::class, inversedBy="players")
*/
private $playedMatch;
/**
* @ORM\ManyToOne(targetEntity=Position::class, inversedBy="matches")
*/
private $position;
// ...
}
Match.php
class Match
{
// ...
/**
* @ORM\Column(type="smallint")
* @Groups({"match:read"})
*/
private $outcome;
/**
* @ORM\ManyToOne(targetEntity=Sport::class, inversedBy="matches")
*/
private $sport;
/**
* @ORM\OneToMany(targetEntity=MatchPlayer::class, mappedBy="playedMatch", cascade={"persist", "remove"})
*/
private $players;
// ....
}
So in my model, a user can relate to many matches and a match can relate to many users via the glue table that also saves what position a user played.
Now I want to expose an endpoint with api platform like/api/users/{id}/statistics
or/api/statistics/{userId}
that fetches data dynamically and shows how many matches a user has played in which sport, on what position, and how many matches the user has won/tied/lost. Ideally, the endpoint would allow filtering by sports and would look something like/api/users/{id}/statistics?sport[]=football&sport[]&outcome=win
for example.
Because these statistics don't get persisted to the database as an entity, I tried an approach similar to the Expose a model without any routes documentation page. I created aStatistics
entity that looks like this:
/**
* @ApiResource(
* collectionOperations={},
* itemOperations={
* "get"={
* "controller"=NotFoundAction::class,
* "read"=false,
* "output"=false,
* },
* }
* )
*/
class Statistic
{
/**
* @var User
* @ApiProperty(identifier=true)
*/
public $user;
/**
* @var Position[]|null
*/
public $position = [];
/**
* @var Sport[]|null
*/
public $maps = [];
/**
* @var int
*/
public $wins = 0;
/**
* @var int
*/
public $ties = 0;
/**
* @var int
*/
public $losses = 0;
}
and added a custom operation to theUser
entity:
* @ApiResource(
* ...
* itemOperations={
* "get_statistic"={
* "method"="GET",
* "path"="/users/{id}/statistics",
* }
* },
* ...
*/
However I am not sure how to implement the filtering by sports, position and wins/ties/losses. A "normal" filter doesn't work as far as I know since its only applied to the get operation on collections.
If this is even possible, how would I implement this in my api? I already tried custom data providers and controllers, but I cant get the filter query parameters in either solution, and a "normal" filter (like api platforms built in SearchFilter) wont work since it is only applied to the get operation on collections, and I am dealing with an item.
It's definitely possible, but depending on your choice there's more work you need to do to get the desired result.
I'll go with custom operation since that's easier to explain and I already have some code examples.
To get the information you need for filtering you'll need to go with a lower level approach. The key part that you missed, is that API Platform is built on top of Symfony, so you can just use theRequest
(for a custom operation) or theRequestStack
(for a data provider) to get the filters.
Also to make sure API Platform knows how to serialize the data you are outputting (Statistics
object), you'll need to use a DTO.
Here's how the code would look like:
On your entity, we add the custom operation class and specify the output as the Statistics class:
* @ApiResource(
* ...
* itemOperations={
* "get_statistics"={
* "method"="GET",
* "path"="/users/{id}/statistics",
* "controller"=UserStatsAction::class,
* "input"=Statistics::class
* }
* },
* ...
*/
The custom operation code sample:
final class UserStatsAction
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function __invoke(Request $request)
{
$id = $request->get('id');
$repository = $this->em->getRepository(User::class);
if(!($user = $repository->find($id))) {
throw new NotFoundHttpException();
}
$sports = $request->query->get('sport', []);
$outcome = $request->query->get('outcome');
// Optional: validate your filter data
$validator = Validation::createValidator();
$context = $validator->startContext();
$context->atPath('sports')->validate($sports, [
new Assert\Choice([
'choices' => ['football', 'basketball'],
]),
]);
$context->atPath('outcome')->validate($outcome, [
new Assert\Choice([
'choices' => ['win', 'loose', 'tie'],
]),
]);
$violations = $context->getViolations();
if (0 !== count($violations)) {
throw new ValidationException($violations);
}
// I'll assume you are hnadiling empty/nulls value properly inside this method
// and return all the stats if
$results = $repository->getStatistics($sports, $outcome);
// For this to work, you'll need to set a DTO for your stats
return $results;
}
}
I'm using theRequest
as argument for custom operation, not theUser
entity. There is some code in my example that you might not need, like fetching the user from the repository or validating the filters (I do encourage user input cleanup/validation though).
One important mention: custom operations are discouraged by API Platform, and you'll lose GraphQL support. If you need GraphQL, the same result can be accomplished with aDataProvider
but that's a more advanced setup and I'll need to mock some parts of your app to figure it out.
Hope this helps.
Update:
For filters to work you'll need to also update the OpenAPI/Swagger configuration, as tobias ingold pointed out in the comment below.
You can do that using PHP and creating a Normalizer, as described in the Override the OpenAPI speficiation section of the docs.
This can also be done by expanding on theAPIResource
annotation, here's an example:
* @ApiResource(
* ...
* collectionOperations={
* "post",
* "get"={
* "openapi_context"={
* "parameters"={
* {
* "name": "<query_string_param_name>",
* "type": "string",
* "in": "query",
* "required": false,
* "description": "description",
* "example": ""
* }
* }
* }
* }
* }
* ...
* })
I found this approach easier to use, but it's not documented. I extrapolated this based on my OpenAPI spec knowledge and the Configuring the Entity Receiving the Uploaded File example in the official documentation.
Our community is visited by hundreds of web development professionals every day. Ask your question and get a quick answer for free.
Find the answer in similar questions on our website.
Do you know the answer to this question? Write a quick response to it. With your help, we will make our community stronger.
PHP (from the English Hypertext Preprocessor - hypertext preprocessor) is a scripting programming language for developing web applications. Supported by most hosting providers, it is one of the most popular tools for creating dynamic websites.
The PHP scripting language has gained wide popularity due to its processing speed, simplicity, cross-platform, functionality and distribution of source codes under its own license.
https://www.php.net/
Symfony compares favorably with other PHP frameworks in terms of reliability and maturity. This framework appeared a long time ago, in 2005, that is, it has existed much longer than most of the other tools we are considering. It is popular for its web standards compliance and PHP design patterns.
https://symfony.com/
Welcome to the Q&A site for web developers. Here you can ask a question about the problem you are facing and get answers from other experts. We have created a user-friendly interface so that you can quickly and free of charge ask a question about a web programming problem. We also invite other experts to join our community and help other members who ask questions. In addition, you can use our search for questions with a solution.
Ask about the real problem you are facing. Describe in detail what you are doing and what you want to achieve.
Our goal is to create a strong community in which everyone will support each other. If you find a question and know the answer to it, help others with your knowledge.