If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe're going to start doing more with our API... so it's time to bring this thing to life with some data fixtures!
For this, I like to use Foundry along with DoctrineFixturesBundle. So, run
composer require foundry orm-fixtures --dev
to install both as dev
dependencies. Once that finishes, run
php bin/console make:factory
If you haven't used Foundry before, for each entity, you create a factory class that's really good at creating that entity. I'll hit zero to generate the one for DragonTreasure
.
The end result is a new src/Factory/DragonTreasureFactory.php
file:
... lines 1 - 2 | |
namespace App\Factory; | |
use App\Entity\DragonTreasure; | |
use App\Repository\DragonTreasureRepository; | |
use Zenstruck\Foundry\ModelFactory; | |
use Zenstruck\Foundry\Proxy; | |
use Zenstruck\Foundry\RepositoryProxy; | |
/** | |
* @extends ModelFactory<DragonTreasure> | |
* | |
* @method DragonTreasure|Proxy create(array|callable $attributes = []) | |
* @method static DragonTreasure|Proxy createOne(array $attributes = []) | |
* @method static DragonTreasure|Proxy find(object|array|mixed $criteria) | |
* @method static DragonTreasure|Proxy findOrCreate(array $attributes) | |
* @method static DragonTreasure|Proxy first(string $sortedField = 'id') | |
* @method static DragonTreasure|Proxy last(string $sortedField = 'id') | |
* @method static DragonTreasure|Proxy random(array $attributes = []) | |
* @method static DragonTreasure|Proxy randomOrCreate(array $attributes = []) | |
* @method static DragonTreasureRepository|RepositoryProxy repository() | |
* @method static DragonTreasure[]|Proxy[] all() | |
* @method static DragonTreasure[]|Proxy[] createMany(int $number, array|callable $attributes = []) | |
* @method static DragonTreasure[]|Proxy[] createSequence(array|callable $sequence) | |
* @method static DragonTreasure[]|Proxy[] findBy(array $attributes) | |
* @method static DragonTreasure[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) | |
* @method static DragonTreasure[]|Proxy[] randomSet(int $number, array $attributes = []) | |
*/ | |
final class DragonTreasureFactory extends ModelFactory | |
{ | |
... lines 32 - 36 | |
public function __construct() | |
{ | |
parent::__construct(); | |
} | |
... lines 41 - 46 | |
protected function getDefaults(): array | |
{ | |
return [ | |
'coolFactor' => self::faker()->randomNumber(), | |
'description' => self::faker()->text(), | |
'isPublished' => self::faker()->boolean(), | |
'name' => self::faker()->text(255), | |
'plunderedAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()), | |
'value' => self::faker()->randomNumber(), | |
]; | |
} | |
... lines 58 - 61 | |
protected function initialize(): self | |
{ | |
return $this | |
// ->afterInstantiate(function(DragonTreasure $dragonTreasure): void {}) | |
; | |
} | |
protected static function getClass(): string | |
{ | |
return DragonTreasure::class; | |
} | |
} |
This class is just really good at creating DragonTreasure
objects. It even has a bunch of nice random data ready to be used!
To make this even fancier, I'm going to paste over with some code that I've dragon-ized. Oh, and we also need a TREASURE_NAMES
constant... which I'll also paste on top. You can grab all of this from the code block on this page.
... lines 1 - 29 | |
final class DragonTreasureFactory extends ModelFactory | |
{ | |
private const TREASURE_NAMES = ['pile of gold coins', 'diamond-encrusted throne', 'rare magic staff', 'enchanted sword', 'set of intricately crafted goblets', 'collection of ancient tomes', 'hoard of shiny gemstones', 'chest filled with priceless works of art', 'giant pearl', 'crown made of pure platinum', 'giant egg (possibly a dragon egg?)', 'set of ornate armor', 'set of golden utensils', 'statue carved from a single block of marble', 'collection of rare, antique weapons', 'box of rare, exotic chocolates', 'set of ornate jewelry', 'set of rare, antique books', 'giant ball of yarn', 'life-sized statue of the dragon itself', 'collection of old, used toothbrushes', 'box of mismatched socks', 'set of outdated electronics (such as CRT TVs or floppy disks)', 'giant jar of pickles', 'collection of novelty mugs with silly sayings', 'pile of old board games', 'giant slinky', 'collection of rare, exotic hats']; | |
... lines 33 - 46 | |
protected function getDefaults(): array | |
{ | |
return [ | |
'coolFactor' => self::faker()->numberBetween(1, 10), | |
'description' => self::faker()->paragraph(), | |
'isPublished' => self::faker()->boolean(), | |
'name' => self::faker()->randomElement(self::TREASURE_NAMES), | |
'plunderedAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTimeBetween('-1 year')), | |
'value' => self::faker()->numberBetween(1000, 1000000), | |
]; | |
} | |
... lines 58 - 72 | |
} |
Ok, so this class is done. Step two: to actually create some fixtures, open src/DataFixtures/AppFixtures.php
. I'll clear out the load()
method. All we need is: DragonTreasureFactory::createMany(40)
to create a healthy trove of 40 treasures:
... lines 1 - 2 | |
namespace App\DataFixtures; | |
use App\Factory\DragonTreasureFactory; | |
use Doctrine\Bundle\FixturesBundle\Fixture; | |
use Doctrine\Persistence\ObjectManager; | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
DragonTreasureFactory::createMany(40); | |
} | |
} |
Let's try this thing! Back at your terminal, run:
symfony console doctrine:fixtures:load
Say "yes" and... it looks like it worked! Back on our API docs, refresh... then let's try the GET
collection endpoint. Hit execute.
Oh, so cool! Look at all those beautiful treasures! Remember, we added 40. But if you look closely... even though the IDs
don't start at 1, we can see that there are definitely less than 40 here. The response says hydra:totalItems: 40
, but it only shows 25.
Down here, this hydra:view
kind of explains why: there's built-in pagination! Right now we're looking at page 1.. and we can also see the URLs for the last page and the next page.
So yes, API endpoints that return a collection need pagination... just like a website. And with API Platform, it just works.
To play with this, let's go to /api/treasures.jsonld
. This is page 1... and then we can add ?page=2
to see page 2. That's the easiest thing I'll do all day.
Now if you need to, you can change a bunch of pagination options. Let's see if we can tweak the number of items per page from 25 to 10.
To start digging into the config, open up your terminal and run:
php bin/console debug:config api_platform
There are a lot of things that you can configure on API Platform. And this command shows us the current configuration. So for example, you can add a title
and description
to your API. This becomes part of the OpenAPI Spec... and so it shows up on your documentation.
If you search for pagination
- we don't want the one under graphql
... we want the one under collection
- we can see several pagination-related options. But, again, this is showing us the current configuration... it doesn't necessarily show us all possible keys.
To see that, instead of debug:config
, run:
php bin/console config:dump api_platform
debug:config
shows you the current configuration. config:dump
shows you a full tree of possible configuration. Now... we see pagination_items_per_page
. That sounds like what we want!
This is actually really cool. All of these options live under something called defaults
. And these are snake-case versions of the exact same options that you'll find inside the ApiResource
attribute. Setting any of these defaults
in the config causes that to be the default value passed to that option for every ApiResource
in your system. Pretty cool.
So, if we wanted to change the items per page globally, we could do it with this config. Or, if we want to change it only for one resource, we can do it above the class.
Find the ApiResource
attribute and add paginationItemsPerPage
set to 10:
... lines 1 - 18 | |
( | |
... lines 20 - 34 | |
paginationItemsPerPage: 10 | |
) | |
class DragonTreasure | |
{ | |
... lines 39 - 161 | |
} |
Again, you can see that the options we already have... are included in the defaults
config.
Move over and head back to page 1. And... voilà! A much shorter list. Also, there are now 4 pages of treasure instead of 2.
Oh, and FYI: you can also make it so that the user of your API can determine how many items to show per page via a query parameter. Check the documentation for how to do that.
Ok, now that we have a lot of data, let's add the ability for our Dragon API users to search and filter through the treasures. Like maybe a dragon is searching for a a treasure of individually wrapped candies among all this loot. That's next.
Hey Mickael,
We're sorry to hear you're experiencing trouble accessing this video. This happens sometimes when the video was on pause for a long time. The full page refresh should help. If not - try to download the video and watch it locally. I just double-checked the video - it works great for me, so it might be a temporary outage of Vimeo - the hosting provider behind our videos - or something related to your local network. Anyway, downloading the video and watching it locally should help. Let us know if you still have problems with it!
Cheers!
What do I need to do to have a custom paginator (or DataProvider??) so that I can pass an offset and limit instead of items_per_page and a page_number?
Javascript code that I have no control over expects a slice of the data, not a page.
As a hack, I passed a "start" parameter (which will of course be seen as a filter), and replaced the offset (leaving items_per_page along, as that functions as the limit too). This is exactly what I want -- to control the starting offset, not the page.
Doctrine\Orm\Extension\PaginationExtension,php
[$offset, $limit] = $pagination;
$offset = $context['filters']['start'];
More details at https://github.com/api-platform/core/issues/4672 -- I'd LOVE to be able to solve this problem, as I have a hack that does two larger-than-necessary API calls and then slices the result as needed. Ugh.
Hey @Tac-Tacelosky
Perhaps you need a cursor or partial pagination, you can learn more about them in the docs https://api-platform.com/docs/core/pagination/#cursor-based-pagination
Also, remember that you can "decorate" APIPlatform services to extend/modify their behavior
Cheers!
Hi!
I am working on an API Platform 3 (Symfony 6) app.
In my JSON response, I have the following :
{ ... "totalItems": 7065, "itemsPerPage": 10, ... }
Is it possible to change the config (or do something else) so that I get :
{ ... "total_items": 7065, "page_size": 10, ... }
So basically I want to rename these fields, in the JSON response. Is it possible ?
I emphasize that I want to override the default fields (totalItems and itemsPerPage) in the JSON response.
Thanks!
Hey Marko,
I couldn't find any config to change those field names in the documentation. So, what I think you could do is create an event listener and override those fields directly on the response data. https://api-platform.com/docs/core/events/
Cheers!
Hello! Thanks a lot for your answer. Actually, according to this link, GraphQL is not supported by the event system. Given that we may actually migrate from REST to GraphQL in the future, do you think there is another way to override these field names ? Because, with GraphQL, what we implemented with events will not work anymore. Maybe we can use some normalizer or anything else, compatible with GraphQL, to achieve the same goal ?..
It actually worked with a custom normalizer. According to API Platform doc, unlike event system, normalizers & denormalizers are GraphQL friendly
Hey Marko,
Oh, I didn't know that about GraphQL. Anyways, I'm glad to know you could solve your problem.
By the way, remember that you can "decorate" any services to add extra behavior to them https://api-platform.com/docs/core/extending/#leveraging-the-built-in-infrastructure-using-composition
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.0", // v3.0.8
"doctrine/annotations": "^1.0", // 1.14.2
"doctrine/doctrine-bundle": "^2.8", // 2.8.0
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.14", // 2.14.0
"nelmio/cors-bundle": "^2.2", // 2.2.0
"nesbot/carbon": "^2.64", // 2.64.1
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.15.3
"symfony/asset": "6.2.*", // v6.2.0
"symfony/console": "6.2.*", // v6.2.3
"symfony/dotenv": "6.2.*", // v6.2.0
"symfony/expression-language": "6.2.*", // v6.2.2
"symfony/flex": "^2", // v2.2.4
"symfony/framework-bundle": "6.2.*", // v6.2.3
"symfony/property-access": "6.2.*", // v6.2.3
"symfony/property-info": "6.2.*", // v6.2.3
"symfony/runtime": "6.2.*", // v6.2.0
"symfony/security-bundle": "6.2.*", // v6.2.3
"symfony/serializer": "6.2.*", // v6.2.3
"symfony/twig-bundle": "6.2.*", // v6.2.3
"symfony/ux-react": "^2.6", // v2.6.1
"symfony/validator": "6.2.*", // v6.2.3
"symfony/webpack-encore-bundle": "^1.16", // v1.16.0
"symfony/yaml": "6.2.*" // v6.2.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
"symfony/debug-bundle": "6.2.*", // v6.2.1
"symfony/maker-bundle": "^1.48", // v1.48.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/stopwatch": "6.2.*", // v6.2.0
"symfony/web-profiler-bundle": "6.2.*", // v6.2.4
"zenstruck/foundry": "^1.26" // v1.26.0
}
}
I can't play the video (lesson 12), this is the error : The media could not be loaded, either because the server or network failed or because the format is not supported.