ApiToken Entity

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time to get to work on our API token authentication system! As we just learned, there are a bunch of different ways to do API auth. We're going to code through one way, which will make you plenty dangerous for whatever way you ultimately need.

For our API tokens, we're going to create an ApiToken entity in the database to store them. Find your terminal and run:

php bin/console make:entity

Call the class ApiToken. And, we need a few fields: token, a string that's not nullable, expiresAt so that we can set an expiration as a datetime, and user, which will be a relation type to our User class. In this situation, we want a ManyToOne relationship so that each ApiToken has one User and each User can have many ApiTokens. Make this not nullable: every API token must be related to a User. And, though it doesn't matter for authentication, let's map both sides of the relationship. That will allow us to easily fetch all of the API tokens for a specific user. For orphanRemoval, this is also not important, but choose yes. If we create a page where a user can manage their API tokens, this might make it easier to delete API tokens.

And... done!

... lines 1 - 2
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\ApiTokenRepository")
*/
class ApiToken
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $token;
/**
* @ORM\Column(type="datetime")
*/
private $expiresAt;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="apiTokens")
* @ORM\JoinColumn(nullable=false)
*/
private $user;
public function getId(): ?int
{
return $this->id;
}
public function getToken(): ?string
{
return $this->token;
}
public function setToken(string $token): self
{
$this->token = $token;
return $this;
}
public function getExpiresAt(): ?\DateTimeInterface
{
return $this->expiresAt;
}
public function setExpiresAt(\DateTimeInterface $expiresAt): self
{
$this->expiresAt = $expiresAt;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
}

Generate the migration with:

php bin/console make:migration

Go check it out - in the Migrations/ directory, open that file:

... lines 1 - 2
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20180901171717 extends AbstractMigration
{
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE api_token (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, token VARCHAR(255) NOT NULL, expires_at DATETIME NOT NULL, INDEX IDX_7BA2F5EBA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE api_token ADD CONSTRAINT FK_7BA2F5EBA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE api_token');
}
}

Cool! CREATE TABLE api_token with id, user_id, token and expires_at. And, it creates the foreign key.

That looks perfect. Move back and run it!

php bin/console doctrine:migrations:migrate

How are Tokens Created?

So, the question of how these ApiTokens will be created is not something we're going to answer. As we talked about, it's either super easy... or super complicated, depending on your needs.

So, for our app, we're just going to create some ApiTokens via the fixtures.

Making the ApiToken Class Awesome

But before we do that, open the new ApiToken entity class. Yep, all the usual stuff: some properties, annotations and a getter & setter for each method. I want to change things a bit. The make:entity command always generates getter and setter methods. But, in some cases, there is a better way to design things.

Add a public function __construct() method with a User argument:

... lines 1 - 9
class ApiToken
{
... lines 12 - 34
public function __construct(User $user)
{
... lines 37 - 39
}
... lines 41 - 60
}

Because every ApiToken needs a User, why not make it required when the object is instantiated? Oh, and we can also generate the random token string here. Use $this->token = bin2hex(random_bytes(60)). Then $this->user = $user:

... lines 1 - 9
class ApiToken
{
... lines 12 - 34
public function __construct(User $user)
{
$this->token = bin2hex(random_bytes(60));
$this->user = $user;
... line 39
}
... lines 41 - 60
}

Oh, and we can also set the expires time here - $this->expiresAt = new \DateTime() with +1 hour:

... lines 1 - 9
class ApiToken
{
... lines 12 - 34
public function __construct(User $user)
{
$this->token = bin2hex(random_bytes(60));
$this->user = $user;
$this->expiresAt = new \DateTime('+1 hour');
}
... lines 41 - 60
}

You can set the expiration time for however long you want.

Now that we are initializing everything in the constructor, we can clean up the class: remove all the setter methods:

... lines 1 - 9
class ApiToken
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $token;
/**
* @ORM\Column(type="datetime")
*/
private $expiresAt;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="apiTokens")
* @ORM\JoinColumn(nullable=false)
*/
private $user;
public function __construct(User $user)
{
$this->token = bin2hex(random_bytes(60));
$this->user = $user;
$this->expiresAt = new \DateTime('+1 hour');
}
public function getId(): ?int
{
return $this->id;
}
public function getToken(): ?string
{
return $this->token;
}
public function getExpiresAt(): ?\DateTimeInterface
{
return $this->expiresAt;
}
public function getUser(): ?User
{
return $this->user;
}
}

Yep, our token class is now immutable, which wins us major hipster points. Immutable just means that, once it's instantiated, this object's data can never be changed. Some developers think that making immutable objects like this is super important. I don't fully agree with that. But, it definitely makes sense to be thoughtful about your entity classes. Sometimes having setter methods makes sense. But sometimes, it makes more sense to setup some things in the constructor and remove the setter methods if you don't need them.

Oh, and if, in the future, you want to update the data in this entity - maybe you need to change the expiresAt, it's totally OK to add a new public function to allow that. But, when you do, again, be thoughtful. You could add a public function setExpiresAt(). Or, if all you ever do is re-set the expiresAt to one hour from now, you could instead create a public function renewExpiresAt() that handles that logic for you:

public function renewExpiresAt()
{
    $this->expiresAt = new \DateTime('+1 hour');
}

That method name is more meaningful, and centralizes more control inside the class.

Ok, I'm done with my rant!

Adding ApiTokens to the Fixtures

Let's create some ApiTokens in the fixtures already! We could create a new ApiTokenFixture class, but, to keep things simple, I'm going to put the logic right inside UserFixture.

Use $apiToken1 = new ApiToken() and pass our User. Copy that and create $apiToken2:

... lines 1 - 4
use App\Entity\ApiToken;
... lines 6 - 9
class UserFixture extends BaseFixture
{
... lines 12 - 18
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) use ($manager) {
... lines 22 - 34
$apiToken1 = new ApiToken($user);
$apiToken2 = new ApiToken($user);
... lines 37 - 39
return $user;
});
... lines 42 - 57
}
}

With our fancy createMany() method, you do not need to call persist() or flush() on the object that you return. That's because our base class calls persist() on the object for us:

... lines 1 - 9
abstract class BaseFixture extends Fixture
{
... lines 12 - 45
protected function createMany(int $count, string $groupName, callable $factory)
{
for ($i = 0; $i < $count; $i++) {
... lines 49 - 54
$this->manager->persist($entity);
... lines 56 - 58
}
}
... lines 61 - 90
}

But, if you create some objects manually - like this - you do need to call persist(). No big deal: add use ($manager) to make the variable available in the callback. Then,$manager->persist($apiToken1) and $manager->persist($apiToken2):

... lines 1 - 4
use App\Entity\ApiToken;
... lines 6 - 9
class UserFixture extends BaseFixture
{
... lines 12 - 18
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) use ($manager) {
... lines 22 - 34
$apiToken1 = new ApiToken($user);
$apiToken2 = new ApiToken($user);
$manager->persist($apiToken1);
$manager->persist($apiToken2);
... lines 39 - 40
});
... lines 42 - 57
}
}

That should be it! Let's reload some fixtures!

php bin/console doctrine:fixtures:load

When it's done, run:

php bin/console doctrine:query:sql 'SELECT * FROM api_token'

Beautiful, long, random strings. And each is related to a User.

Next, let's create an authenticator that's capable of reading, processing & authenticating these API tokens.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.9.10
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}