Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Creating your First ApiResource

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

We're about to build an API for the very important job of allowing dragons to show off their treasure. Right now, our project doesn't have a single database entity... but we're going to need one to store all that treasure.

Generating our First Entity

Find your terminal and first run

composer require maker --dev

to install Maker Bundle. Then run:

php bin/console make:entity

Perfect! Let's call our entity DragonTreasure. Then it asks us a question that you maybe haven't seen before - Mark this class as an API platform resource? It asks because API Platform is installed. Say no because we're going to do this step manually in a moment.

Okay, let's start adding properties. Start with name as a string, with a Length of the default 255, and make it not nullable. Then, add description with a text type, and make that not nullable. We also need a value, like... how much the treasure is worth. That will be an integer not nullable. And we simply must have a coolFactor: dragons need to specify just how awesome this treasure is. That'll be a number from 1 to 10, so make it an integer and not nullable. Then, createdAt datetime_immutable that's not nullable... and Finally, add an isPublished property, which will be a boolean type, also not nullable. Hit " enter" to finish.

Phew! There's nothing very special so far. This created two classes: DragonTreasureRepository (which we're not going to worry about), and the DragonTreasure entity itself with $id, $name, $description, $value, etc

... lines 1 - 2
namespace App\Entity;
use App\Repository\DragonTreasureRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: DragonTreasureRepository::class)]
class DragonTreasure
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $description = null;
#[ORM\Column]
private ?int $value = null;
#[ORM\Column]
private ?int $coolFactor = null;
#[ORM\Column]
private ?\DateTimeImmutable $plunderedAt = null;
#[ORM\Column]
private ?bool $isPublished = null;
... lines 35 - 110
}

along with the getter and setter methods. Beautifully boring. There is one little bug in this version of MakerBundle, though. It generated an isIsPublished() method. Let's change that to getIsPublished().

... lines 1 - 9
class DragonTreasure
{
... lines 12 - 99
public function getIsPublished(): ?bool
{
return $this->isPublished;
}
public function setIsPublished(bool $isPublished): self
{
... lines 107 - 109
}
}

Setting up the Database

All right, so we have our entity. Now we need a migration for its table... but that might be a bit difficult since we don't have our database set up yet! I'm going to use Docker for this. The DoctrineBundle recipe gave us a nice docker-compose.yml file that boots up Postgres, so... let's use that! Spin over to your terminal and run:

docker-compose up -d

If you don't want to use Docker, feel free to start your own database engine and then, in .env or .env.local, configure DATABASE_URL. Because I'm using Docker as well as the symfony binary, I don't need to configure anything. The Symfony web server will automatically see the Docker database and set the DATABASE_URL environment variable for me.

Okay, to make the migration, run:

symfony console make:migration

This symfony console is just like ./bin/console except it injects the DATABASE_URL environment variable so that the command can talk to the Docker database. Perfect! Spin over and check out the new migration file... just to make sure it doesn't contain any weird surprises.

... lines 1 - 4
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230104160057 extends AbstractMigration
{
... lines 15 - 19
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SEQUENCE dragon_treasure_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE dragon_treasure (id INT NOT NULL, name VARCHAR(255) NOT NULL, description TEXT NOT NULL, value INT NOT NULL, cool_factor INT NOT NULL, plundered_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_published BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('COMMENT ON COLUMN dragon_treasure.plundered_at IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE dragon_treasure_id_seq CASCADE');
$this->addSql('DROP TABLE dragon_treasure');
}
}

Looks good! So spin back over and run this with:

symfony console doctrine:migrations:migrate

Done!

Exposing our First API Resource

We now have an entity and a database table. But if you go and refresh the documentation... there's still nothing there. What we need to do is tell API Platform to expose our DragonTreasure entity as an API resource. To do this, go above the class and add a new attribute called ApiResource. Hit "tab" to add that use statement.

... lines 1 - 4
use ApiPlatform\Metadata\ApiResource;
... lines 6 - 9
#[ORM\Entity(repositoryClass: DragonTreasureRepository::class)]
#[ApiResource]
class DragonTreasure
{
... lines 14 - 112
}

Done! As soon as we do that... and refresh... whoa! The documentation is alive! It now shows that we have six different endpoints: One to retrieve all of the DragonTreasure resources, one to retrieve an individual DragonTreasure, one to create a DragonTreasure, two that edit a DragonTreasure plus one to delete it. And this is more than just documentation. These endpoints work.

Go over and click "Try it Out", then "Execute". It doesn't actually return anything because our database is empty, but it does gives us a 200 status code with some empty JSON. We'll talk about all of the other fancy keys in the response shortly.

Oh, but I do want to mention one thing. As we just saw, the easiest way to create a set of API endpoints is by adding this ApiResource attribute above your entity class. But you can actually add this attribute above any class: not just entities. That's something we're going to talk about in a future tutorial: it can be a nice way to separate what your API looks like from what your entity looks like, especially in bigger APIs. But again, that's for later. Right now, using ApiResource on top of our entity is going to work great.

Let's discover this cool, interactive documentation a bit more. Where did this come from? How does our app magically have a bunch of new routes? And do dragons really love tacos? Let's find out next!

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// 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
    }
}