Skip to content

Instantly share code, notes, and snippets.

@Gerardlc
Forked from GrzegorzBandur/Readme.md
Created March 1, 2019 13:34
Show Gist options
  • Save Gerardlc/40883dd069ada5f3e429abc57fadf3eb to your computer and use it in GitHub Desktop.
Save Gerardlc/40883dd069ada5f3e429abc57fadf3eb to your computer and use it in GitHub Desktop.
RESTful API with Symfony 4 + FOSRestBundle + FOSOauthServerBundle + FOSUserBundle

RESTful API with Symfony 4 + FOSRestBundle + FOSOauthServerBundle + FOSUserBundle

To start writing RestFull API in symfony we will need bundles:

"friendsofsymfony/oauth-server-bundle": "1.6",
"friendsofsymfony/rest-bundle": "^2.3",
"friendsofsymfony/user-bundle": "^2.1",
"jms/serializer-bundle": "^2.4",
"nelmio/api-doc-bundle": "^2.13",
"sensio/framework-extra-bundle": "^5.2",
"symfony/apache-pack": "^1.0",
"symfony/console": "^4.1",
"symfony/flex": "^1.0",
"symfony/framework-bundle": "^4.1",
"symfony/lts": "^4@dev",
"symfony/maker-bundle": "^1.5",
"symfony/orm-pack": "^1.0",
"symfony/swiftmailer-bundle": "^3.2",
"symfony/templating": "^4.1",
"symfony/yaml": "^4.1"

So let’s get started:

Firstly we create project in symfony. To do this we will need composer you can download and install from page:

https://getcomposer.org/download/

We can create skeleton project in symfony using command prompt:

create-project symfony/skeleton rest

But more simple is using this composer.json create project running command composer install.

{
    "type": "project",
    "license": "proprietary",
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "friendsofsymfony/oauth-server-bundle": "1.6",
        "friendsofsymfony/rest-bundle": "^2.3",
        "friendsofsymfony/user-bundle": "^2.1",
        "jms/serializer-bundle": "^2.4",
        "nelmio/api-doc-bundle": "^2.13",
        "sensio/framework-extra-bundle": "^5.2",
        "symfony/apache-pack": "^1.0",
        "symfony/console": "^4.1",
        "symfony/flex": "^1.0",
        "symfony/framework-bundle": "^4.1",
        "symfony/lts": "^4@dev",
        "symfony/maker-bundle": "^1.5",
        "symfony/orm-pack": "^1.0",
        "symfony/swiftmailer-bundle": "^3.2",
        "symfony/templating": "^4.1",
        "symfony/yaml": "^4.1"
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0",
        "symfony/dotenv": "^4.1",
        "symfony/var-dumper": "^4.1"
    },
    "config": {
        "preferred-install": {
            "*": "dist"
        },
        "sort-packages": true
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "replace": {
        "symfony/polyfill-ctype": "*",
        "symfony/polyfill-iconv": "*",
        "symfony/polyfill-php71": "*",
        "symfony/polyfill-php70": "*",
        "symfony/polyfill-php56": "*"
    },
    "scripts": {
        "auto-scripts": {
            "cache:clear": "symfony-cmd",
            "assets:install %PUBLIC_DIR%": "symfony-cmd"
        },
        "post-install-cmd": [
            "@auto-scripts"
        ],
        "post-update-cmd": [
            "@auto-scripts"
        ]
    },
    "conflict": {
        "symfony/symfony": "*"
    },
    "extra": {
        "symfony": {
            "allow-contrib": false
        }
    }
}

Then we got a question Do you want to execute this recipe? Hit a. Because our config files isn't propertly filled. To work this propertly we need to create some config files under the config/packages folder.

fos_user.yml 
    fos_user:
      db_driver: orm # other valid values are 'mongodb' and 'couchdb'
      firewall_name: main
      user_class: App\Entity\User
      from_email:
        address: '[email protected]' # for example your email
        sender_name: '[email protected]' # for example your email

fos_oauth_user.yml

fos_oauth_server:
    db_driver: orm
    client_class:        App\Entity\Client
    access_token_class:  App\Entity\AccessToken
    refresh_token_class: App\Entity\RefreshToken
    auth_code_class:     App\Entity\AuthCode
    service:
        user_provider: fos_user.user_provider.username
        options:
            access_token_lifetime: 28800
    template:
        engine: twig

These class we use to store authcode and access tokens to authorise to our API.

We also need to as to the framework.yml templating:

framework:
    templating:
        engines: ['twig']

And also nelmio_api_doc.yaml

nelmio_api_doc:
  sandbox:
    enabled: true
    authentication:
      delivery: http
      type: bearer
    body_format:
      formats: [ json, form ]             # array of enabled body formats,
      default_format: json
  swagger:
    api_base_path:        /api
    swagger_version:      '1.2'
    api_version:          '0.1'
    info:
      title:                Rest
      description:          'Rest app in symfony 4'

Let’s create fos_oauth_server enity classes.

Offtop: Because we use Symfony 4 and I figured the problem with key length in default mariadb configurations there is two options to resolve very frustrating error. Charset utf8mb4 cause that error when we doctrine try to create key.

SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

One without changing any configuration on the server change config file : Doctrine.yaml and use setting as follows(Simply we will simple utf8 instead utf8mb4):

doctrine:
    dbal:
        # configure these for your database server
        driver: 'pdo_mysql'
        server_version: '5.7'
        charset: utf8
        default_table_options:
            charset: utf8
            collate: utf8_unicode_ci

And second update the database engine on server :)

AccessToken class:

<?php

namespace App\Entity;

use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="oauth2_access_tokens")
 * @ORM\Entity
 */
class AccessToken extends BaseAccessToken
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var Client
     *
     * @ORM\ManyToOne(targetEntity="Client")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $client;

    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User")
     */
    protected $user;
}

AuthCode class

<?php
namespace App\Entity;

use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="oauth2_auth_codes")
 * @ORM\Entity
 */
class AuthCode extends BaseAuthCode
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var Client
     *
     * @ORM\ManyToOne(targetEntity="Client")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $client;

    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User")
     */
    protected $user;
}

Client class

<?php

namespace App\Entity;

use FOS\OAuthServerBundle\Entity\Client as BaseClient;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="oauth2_clients")
 * @ORM\Entity
 */
class Client extends BaseClient
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string
     *
     * @ORM\Column(name="type", type="string", length=150, nullable=true)
     */
    protected $type;

    /**
     * @param mixed $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @param string $type
     */
    public function setType($type)
    {
        $this->type = $type;
    }
}

Refresh token class

<?php
namespace App\Entity;

use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="oauth2_refresh_tokens")
 * @ORM\Entity
 */
class RefreshToken extends BaseRefreshToken
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var Client
     *
     * @ORM\ManyToOne(targetEntity="Client")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $client;

    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User")
     */
    protected $user;
}

These classes are simple. As you can see we also need create User class.

<?php

namespace App\Entity;

use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;

/**
 * User
 *
 * @ORM\Table(name="user", indexes={
 *     @ORM\Index(name="search_idx_username", columns={"username"}),
 *     @ORM\Index(name="search_idx_email", columns={"email"}),
 * })
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 *
 * @UniqueEntity(fields={"email"}, message="EMAIL_IS_ALREADY_IN_USE")
 *
 * @Serializer\ExclusionPolicy("all")
 */
class User extends BaseUser
{
    const ROLE_SUPER_ADMIN = "ROLE_SUPER_ADMIN";
    const ROLE_ADMIN = "ROLE_ADMIN";
    const ROLE_USER = "ROLE_USER";

    /**
     * To validate supported roles
     *
     * @var array
     */
    static public $ROLES_SUPPORTED = array(
        self::ROLE_SUPER_ADMIN => self::ROLE_SUPER_ADMIN,
        self::ROLE_ADMIN => self::ROLE_ADMIN,
        self::ROLE_USER => self::ROLE_USER,
    );

    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string
     *
     * @Assert\NotBlank(message="FIELD_CAN_NOT_BE_EMPTY")
     * @Assert\Email(
     *     message = "INCORRECT_EMAIL_ADDRESS",
     *     checkMX = true
     * )
     */
    protected $email;

    /**
     * @var string
     *
     * @ORM\Column(name="first_name", type="string", length=100, nullable=true)
     *
     * @Assert\Length(
     *      min = 1,
     *      max = 100,
     *      minMessage = "FIELD_LENGTH_TOO_SHORT",
     *      maxMessage = "FIELD_LENGTH_TOO_LONG"
     * )
     */
    private $firstName;

    /**
     * @var string
     *
     * @ORM\Column(name="last_name", type="string", length=100, nullable=true)
     *
     * @Assert\Length(
     *      min = 1,
     *      max = 100,
     *      minMessage = "FIELD_LENGTH_TOO_SHORT",
     *      maxMessage = "FIELD_LENGTH_TOO_LONG"
     * )
     */
    private $lastName;


    /**
     * @var boolean
     *
     * @ORM\Column(name="deleted", type="boolean")
     *
     * @Assert\Type(
     *     type="bool",
     *     message="FIELD_MUST_BE_BOOLEAN_TYPE"
     * )
     */
    private $deleted;

    /**
     * User constructor.
     */
    public function __construct()
    {
        parent::__construct();
        $this->deleted = false;
    }

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set firstName
     *
     * @param string $firstName
     *
     * @return User
     */
    public function setFirstName($firstName)
    {
        $this->firstName = $firstName;

        return $this;
    }

    /**
     * Get firstName
     *
     * @return string
     */
    public function getFirstName()
    {
        return $this->firstName;
    }

    /**
     * Set lastName
     *
     * @param string $lastName
     *
     * @return User
     */
    public function setLastName($lastName)
    {
        $this->lastName = $lastName;

        return $this;
    }

    /**
     * Get lastName
     *
     * @return string
     */
    public function getLastName()
    {
        return $this->lastName;
    }

    /**
     * Set deleted
     *
     * @param boolean $deleted
     *
     * @return User
     */
    public function setDeleted($deleted)
    {
        $this->deleted = $deleted;

        return $this;
    }

    /**
     * Get deleted
     *
     * @return boolean
     */
    public function getDeleted()
    {
        return $this->deleted;
    }
}

And UserRepositiory it can be empty :)

Let’s create some datafixtures

Run command : php bin/console make:fixtures
ClientData

Here we create Oauth2ClientData.

Edit file what we just created.

<?php

namespace App\DataFixtures;

use App\Entity\Client;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Mapping\ClassMetadata;

class ClientData extends Fixture
{
    public function load(ObjectManager $manager)
    {
        $oauth2Client = new Client();

        $oauth2Client->setId(1);
        $oauth2Client->setRandomId('5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s');
        $oauth2Client->setRedirectUris(array());
        $oauth2Client->setSecret('sdgggskokererg4232404gc4csdgfdsgf8s8ck5s');
        $oauth2Client->setAllowedGrantTypes(array('password', 'refresh_token'));

        $manager->persist($oauth2Client);

        /** @var ClassMetadata $metadata */
        $metadata = $manager->getClassMetadata(get_class($oauth2Client));

        $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
        $metadata->setIdGenerator(new AssignedGenerator());

        $manager->flush();
    }
}

And another one with user.

php bin/console make:fixtures
UserData

And like in previews one edit file that we just created.

<?php

namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use FOS\UserBundle\Doctrine\UserManager;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class UserData extends Fixture implements ContainerAwareInterface
{
    const USER_MANAGER = 'fos_user.user_manager';

    /**
     * @var ContainerInterface
     */
    private $container;

    /**
     * @var UserManager
     */
    private $userManager;

    /**
     * @param ContainerInterface|null $container
     */
    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
        $this->userManager = $this->container->get(static::USER_MANAGER);
    }

    public function load(ObjectManager $manager)
    {
        /** @var User $user */
        $user = $this->userManager->createUser();

        $user
            ->setFirstName("Admin")
            ->setLastName("admin")
            ->setEnabled(true)
            ->setRoles(array(User::ROLE_SUPER_ADMIN))
            ->setUsername("admin")
            ->setPlainPassword("admin")
            ->setEmail("[email protected]")
        ;
        $manager->persist($user);
        $manager->flush();
    }
}

Also we need to define password encoder for entity User we can do it in security.yaml by adding these lines

    encoders:
        FOS\UserBundle\Model\UserInterface: sha512

After this run command

php bin/console doctrine:fixtures:load

to update database with datafixtures. We should also define routes routes.yaml

fos_oauth_server_token:
  resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"

fos_oauth_server_authorize:
  resource: "@FOSOAuthServerBundle/Resources/config/routing/authorize.xml"

After this using f.e. built in in PHPStorm HTTPClient we can request a token from our OauthServer

Create POST query to path: /oauth/v2/token giving following parameters:

grant_type-password
client_id=1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40g #notice that we add id_ before client_id which we create using datafixtures
client_secret=sdgggskokererg4232404gc4csdgfdsgf8s8ck5w
username=admin
password=admin

You should get responce like this:

{"access_token":"Zjg2NGFiMWQ4YzMwOGRiMjBkZTE3NzQ0MDdiNGUyYzBhNDFhZDFhN2JmNGNjYzM4YWVlYjYyMjdkODA3OTk3OQ","expires_in":28800,"token_type":"bearer","scope":null,"refresh_token":"NzcxNWZjNTY3ZmFiY2QzMTMyZWE5NmZiOTFlZmJiODg2OTk0ZDA5YmZmODM2ODYxODcxOGI5ZmJmNWIyODg1MA"}

If you get it you probably configured all correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment