To start writing RestFull API in symfony we will need bundles:
"friendsofsymfony/oauth-server-bundle": "^1.6",
"friendsofsymfony/rest-bundle": "^2.7",
"friendsofsymfony/user-bundle": "^2.1",
"jms/serializer-bundle": "^3.5",
"nelmio/api-doc-bundle": "^3.5",
"sensio/framework-extra-bundle": "^5.2",
"symfony/apache-pack": "^1.0",
"symfony/console": "^4.4",
"symfony/dotenv": "4.4.*",
"symfony/flex": "^1.3.1",
"symfony/framework-bundle": "4.4.*",
"symfony/maker-bundle": "^1.5",
"symfony/orm-pack": "^1.0",
"symfony/swiftmailer-bundle": "^3.2",
"symfony/templating": "^4.4",
"symfony/translation": "4.4.*",
"symfony/yaml": "^4.4"
So let’s get started:
Firstly, we create a project in symfony. We will need a composer, which you can download and install from this page/website:
https://getcomposer.org/download/
We can create skeleton project in symfony using command prompt:
create-project symfony/skeleton rest
But the easier way is to use a composer.json to create a project running command: composer install
{
"type": "project",
"license": "proprietary",
"require": {
"php": "^7.3",
"ext-ctype": "*",
"ext-iconv": "*",
"friendsofsymfony/oauth-server-bundle": "^1.6",
"friendsofsymfony/rest-bundle": "^2.7",
"friendsofsymfony/user-bundle": "^2.1",
"jms/serializer-bundle": "^3.5",
"nelmio/api-doc-bundle": "^3.5",
"sensio/framework-extra-bundle": "^5.2",
"symfony/apache-pack": "^1.0",
"symfony/console": "^4.4",
"symfony/dotenv": "4.4.*",
"symfony/flex": "^1.3.1",
"symfony/framework-bundle": "4.4.*",
"symfony/maker-bundle": "^1.5",
"symfony/orm-pack": "^1.0",
"symfony/swiftmailer-bundle": "^3.2",
"symfony/templating": "^4.4",
"symfony/translation": "4.4.*",
"symfony/yaml": "^4.4"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0",
"symfony/dotenv": "^4.4",
"symfony/var-dumper": "^4.4"
},
"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? Press y. After generating config files we correct them. To make it work properly you 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@gmail.com' # for example your email
sender_name: 'email@gmail.com' # 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
We use this class to be able to store the authocode and access the tokens to authorise via our API.
We also need to add to the framework.yml a templating engine:
framework:
templating:
engines: ['twig', 'php']
And add nelmio_api_doc.yaml
nelmio_api_doc:
documentation:
info:
title: My App
description: This is an awesome app!
version: 1.0.0
areas: # to filter documented areas
path_patterns:
- ^/api(?!/doc$) # Accepts routes under /api except /api/doc
Let’s create fos_oauth_server entity classes.
I figured out that the problem with mysql 8.0 in authentication using docker can be solved by adding a command in docker-compose.yaml
command: ['--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci','--default-authentication-plugin=mysql_native_password']
The file doctrine.yaml will look like this
doctrine:
dbal:
# configure these for your database server
driver: 'pdo_mysql'
host: '%env(DB_HOST)%'
port: '%env(DB_PORT)%'
dbname: '%env(DB_DATABASE)%'
user: '%env(DB_USERNAME)%'
password: '%env(DB_PASSWORD)%'
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_ci
AccessToken class:
<?php
declare(strict_types=1);
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
declare(strict_types=1);
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
declare(strict_types=1);
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;
}
Refresh token class
<?php
declare(strict_types=1);
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 to create the User class
<?php
declare(strict_types=1);
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 the UserRepository can be empty :)
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;
use Doctrine\ORM\EntityRepository;
/**
* @method User|null find($id, $lockMode = null, $lockVersion = null)
* @method User|null findOneBy(array $criteria, array $orderBy = null)
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserRepository extends EntityRepository
{
}
Let’s create some datafixtures
Run command : php bin/console make:fixtures
ClientData
Here we create Oauth2ClientData.
Edit the file that we’ve 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();
}
}
Another one with the the user
php bin/console make:fixtures
UserData
Like in the previous one, edit the file that we’ve 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("admin@gmail.com")
;
$manager->persist($user);
$manager->flush();
}
}
We also need to define a password encoder for the entity User. We can do it in security.yaml by adding these lines:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
After this run commands
php bin/console doctrine:database:drop --force --if-exists
php bin/console make:migration
php bin/console doctrine:migrations:migrate
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 response like this:
{"access_token":"Zjg2NGFiMWQ4YzMwOGRiMjBkZTE3NzQ0MDdiNGUyYzBhNDFhZDFhN2JmNGNjYzM4YWVlYjYyMjdkODA3OTk3OQ","expires_in":28800,"token_type":"bearer","scope":null,"refresh_token":"NzcxNWZjNTY3ZmFiY2QzMTMyZWE5NmZiOTFlZmJiODg2OTk0ZDA5YmZmODM2ODYxODcxOGI5ZmJmNWIyODg1MA"}
If you get this result, your configuration is correct.
Wielkie dzięki Grzesiek, świetna robota! Zaoszczędziłeś mi mnóstwo czasu.