Symfony

Symfony

Symfony refers to both a PHP framework for building web applications as well as a set of components on which the framework is built.
Veronica  Roob

Veronica Roob

1653770640

Symfony Dotenv: Parses .env Files

Dotenv Component

Symfony Dotenv parses .env files to make environment variables stored in them accessible via $_SERVER or $_ENV.

Getting Started

$ composer require symfony/dotenv
use Symfony\Component\Dotenv\Dotenv;

$dotenv = new Dotenv();
$dotenv->load(__DIR__.'/.env');

// you can also load several files
$dotenv->load(__DIR__.'/.env', __DIR__.'/.env.dev');

// overwrites existing env variables
$dotenv->overload(__DIR__.'/.env');

// loads .env, .env.local, and .env.$APP_ENV.local or .env.$APP_ENV
$dotenv->loadEnv(__DIR__.'/.env');

Resources

Author: symfony
Source Code: https://github.com/symfony/dotenv
License: MIT License

#php #symfony #dotenv 

Symfony Dotenv: Parses .env Files

NelmioCorsBundle: Adds CORS Headers Support in Your Symfony App

NelmioCorsBundle

About

The NelmioCorsBundle allows you to send Cross-Origin Resource Sharing headers with ACL-style per-URL configuration.

If you want to have a global overview of CORS workflow, you can browse this image.

Features

  • Handles CORS preflight OPTIONS requests
  • Adds CORS headers to your responses

Installation

An official Symfony Flex recipe is available for this bundle. To automatically install and configure it run:

composer req cors

You're done!

Alternatively, if you don't use Symfony Flex, require the nelmio/cors-bundle package in your composer.json and update your dependencies.

$ composer require nelmio/cors-bundle

Add the NelmioCorsBundle to your application's kernel:

    public function registerBundles()
    {
        $bundles = [
            // ...
            new Nelmio\CorsBundle\NelmioCorsBundle(),
            // ...
        ];
        // ...
    }

Configuration

Symfony Flex generates a default configuration in config/packages/nelmio_cors.yaml.

The defaults are the default values applied to all the paths that match, unless overridden in a specific URL configuration. If you want them to apply to everything, you must define a path with ^/.

This example config contains all the possible config values with their default values shown in the defaults key. In paths, you see that we allow CORS requests from any origin on /api/. One custom header and some HTTP methods are defined as allowed as well. Preflight requests can be cached for 3600 seconds.

    nelmio_cors:
        defaults:
            allow_credentials: false
            allow_origin: []
            allow_headers: []
            allow_methods: []
            expose_headers: []
            max_age: 0
            hosts: []
            origin_regex: false
            forced_allow_origin_value: ~
        paths:
            '^/api/':
                allow_origin: ['*']
                allow_headers: ['X-Custom-Auth']
                allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
                max_age: 3600
            '^/':
                origin_regex: true
                allow_origin: ['^http://localhost:[0-9]+']
                allow_headers: ['X-Custom-Auth']
                allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
                max_age: 3600
                hosts: ['^api\.']

allow_origin and allow_headers can be set to * to accept any value, the allowed methods however have to be explicitly listed. paths must contain at least one item.

If origin_regex is set, allow_origin must be a list of regular expressions matching allowed origins. Remember to use ^ and $ to clearly define the boundaries of the regex.

By default, the Access-Control-Allow-Origin response header value is the Origin request header value (if it matches the rules you've defined with allow_origin), so it should be fine for most of use cases. If it's not, you can override this behavior by setting the exact value you want using forced_allow_origin_value.

Be aware that even if you set forced_allow_origin_value to *, if you also set allow_origin to http://example.com, only this specific domain will be allowed to access your resources.

Note: If you allow POST methods and have HTTP method overriding enabled in the framework, it will enable the API users to perform PUT and DELETE requests as well.

Cookbook

How to ignore preflight requests on New Relic?

On specific architectures with a mostly authenticated API, preflight request can represent a huge part of the traffic.

In such cases, you may not need to monitor on New Relic this traffic which is by the way categorized automatically as unknown by New Relic.

A request listener can be written to ignore preflight requests:

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

class PreflightIgnoreOnNewRelicListener
{
    public function onKernelResponse(FilterResponseEvent $event)
    {
        if (!extension_loaded('newrelic')) {
            return;
        }

        if ('OPTIONS' === $event->getRequest()->getMethod()) {
            newrelic_ignore_transaction();
        }
    }
}

Register this listener, and voilà!

Author: Nelmio
Source Code: https://github.com/nelmio/NelmioCorsBundle 
License: MIT License

#php #symfony #cors 

NelmioCorsBundle: Adds CORS Headers Support in Your Symfony App

A symfony bundle to handle multiple upload ways on your REST API

SRIORestUploadBundle

This bundle provide a simple ways to handle uploads on the server side.

Currently, it supports the simple, form-data, multipart and resumable ways.

Getting started

Using Gaufrette as storage layer, you can handle file uploads and store files on many places such as a local file system, an Amazon S3 bucket, ...

Installation

First, you need to install KnpGaufretteBundle, a Symfony integration of Gaufrette which will handle the file storage on places your want.

Add SRIORestUploadBundle in your dependencies

In your composer.json file, add srio/rest-upload-bundle:

{
    "require": {
        "srio/rest-upload-bundle": "~2.0.0"
    }
}

Then, update your dependencies:

composer update srio/rest-upload-bundle

Enable the bundle in your kernel

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        // ...
        new SRIO\RestUploadBundle\SRIORestUploadBundle(),
    );
}

Create the Gaufrette filesystem

In your configuration file, create your Gaufrette filesystem. Let's start with a local filesystem storage in the web/uploads directory.

# app/config/config.yml

knp_gaufrette:
    adapters:
        local_uploads:
            local:
                directory: %kernel.root_dir%/../web/uploads
    filesystems:
        uploads:
            adapter: local_uploads

Configure the bundle

Then, we just have to configure the bundle to use the Gaufrette storage:

srio_rest_upload:
    storages:
        default:
            filesystem: gaufrette.uploads_filesystem

If you want to use the resumable upload way, you have to configure it.

Then, start using it.

Usage

This bundle gives you a srio_rest_upload.upload_handler service that provide an handleRequest method that make all the work for you which returns an UploadResult object.

There's two possible usage:

  • Without form: meaning we just want to upload the file without associated data
  • With form: meaning we also want to add additional data such as name, etc...

In addition to the upload ways documentation there's an AngularJS upload usage example.

Without form

When the file was successfully uploaded, an instance of \SRIO\RestUploadBundle\Storage\UploadedFile can be with getFile on the upload result.

Here's an example controller that handle uploads with the upload handler.

namespace Acme/Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class MediaController extends Controller
{
    public function uploadAction(Request $request)
    {
        $uploadHandler = $this->get('srio_rest_upload.upload_handler');
        $result = $uploadHandler->handleRequest($request);

        if (($response = $result->getResponse()) !== null) {
            return $response;
        }

        if (($file = $result->getFile()) !== null) {
            // Store the file path in an entity, call an API,
            // do whatever with the uploaded file here.

            return new Response();
        }

        throw new BadRequestHttpException('Unable to handle upload request');
    }
}

With form

Because most of the time you may want to link a form to the file upload, you're able to handle it too. Depending on the upload way you're using, form data will be fetched from request body or HTTP parameters.

Here's an example of a controller with a form (it comes directly from tests, feel free to have a look, you'll have all sources):

class UploadController extends Controller
{
    public function uploadAction(Request $request)
    {
        $form = $this->createForm(new MediaFormType());

        /** @var $uploadHandler UploadHandler */
        $uploadHandler = $this->get('srio_rest_upload.upload_handler');
        $result = $uploadHandler->handleRequest($request, $form);

        if (($response = $result->getResponse()) != null) {
            return $response;
        }

        if (!$form->isValid()) {
            throw new BadRequestHttpException();
        }

        if (($file = $result->getFile()) !== null) {
            /** @var $media Media */
            $media = $form->getData();
            $media->setFile($file);

            $em = $this->getDoctrine()->getManager();
            $em->persist($media);
            $em->flush();

            return new JsonResponse($media);
        }

        throw new NotAcceptableHttpException();
    }
}

On the client side

Here's a simple example of an upload (which is using the form-data handler) using AngularJS's $upload service:

$upload.upload({
    url: '/path/to/upload?uploadType=formData',
    method: 'POST',
    file: file
})

Advanced usage

Strategies

You can set naming and storage strategies for each defined storage.

srio_rest_upload:
    storages:
        default:
            filesystem: gaufrette.default_filesystem
            naming_strategy: your_naming_strategy_service
            storage_strategy: your_storage_strategy_service

Naming strategy

The naming strategy is responsible to set the name that the stored file will have. The default naming strategy create a random file name.

To create your own strategy you just have to create a class that implements the NamingStrategy interface. Here's an example with a strategy that generate a random file name but with its extension or the default one as fallback.

namespace Acme\Storage\Strategy;

use SRIO\RestUploadBundle\Upload\UploadContext;
use SRIO\RestUploadBundle\Strategy\NamingStrategy;

class DefaultNamingStrategy implements NamingStrategy
{
    const DEFAULT_EXTENSION = 'png';

    /**
     * {@inheritdoc}
     */
    public function getName(UploadContext $context)
    {
        $name = uniqid();
        $extension = self::DEFAULT_EXTENSION;

        if (($request = $context->getRequest()) !== null) {
            $files = $request->files->all();

            /** @var $file \Symfony\Component\HttpFoundation\File\UploadedFile */
            $file = array_pop($files);

            if ($file !== null) {
                $parts = explode('.', $file->getClientOriginalName());
                $extension = array_pop($parts);
            }
        }

        return $name.'.'.$extension;
    }
}

Then, define a service and change the naming_strategy of your storage configuration with the created service ID.

Storage strategy

It defines the (sub)directory in which the file will be created in your filesystem.

The storage strategy is working the same way than the naming strategy: create a service with a class that implements StorageStrategy and set the storage_strategy configuration of your storage with the created service.

Create a custom handler

You can easily create your custom upload providers (and feel free to PR them on GitHub) by creating a tagged service with the rest_upload.processor tag

<parameters>
    <parameter key="acme.my.processor.class">Acme\AcmeBundle\Processor\MyUploadProcessor</parameter>
</parameters>

<services>
    <service id="acme.my.processor" class="%acme.my.processor.class%">
        <argument type="service" id="doctrine.orm.entity_manager" />
        <tag name="rest_upload.processor" uploadType="acme" />
    </service>
</services>

Note the uploadType attribute that define the unique name of the upload way, set in the uploadType query parameters.

Your MyUploadProcessor class should then implements the ProcessorInterface or extends the AbstractUploadProcessor

Upload ways

This is a summary of currently supported upload ways.

  • Simple: Send binary data to an URL and use query parameters to submit additional data.
  • Multipart: Send both JSON and binary data using the multipart Content-Type.
  • FormData: Matches the classic browser file upload
  • Resumable: start a resumable upload session by sending JSON data and then send file entirely or by chunks. It allow to restart a failed upload where it stops.

Simple upload way

The most straightforward method for uploading a file is by making a simple upload request. This option is a good choice when:

  • The file is small enough to upload again in its entirety if the connection fails
  • There is no or a very small amount of metadata to send

To use simple upload, make a POST or PUT request to the upload method's URI and add the query parameter uploadType=simple. The HTTP headers to use when making a simple upload request include:

  • Content-Type. Set the media content-type
  • Content-Length. Set to the number of bytes you are uploading.

Example

The following example shows the use of a simple photo upload request for an upload path that would be /upload:

POST /upload?uploadType=simple HTTP/1.1
Host: www.example.com
Content-Type: image/jpeg
Content-Length: number_of_bytes_in_JPEG_file

JPEG data

Multipart upload way

If you have metadata that you want to send along with the data to upload, you can make a single multipart/related request. This is a good choice if the data you are sending is small enough to upload again in its entirety if the connection fails. To use multipart upload, make a POST or PUT request to the upload method's URI and add the query parameter uploadType=multipart.

The top-level HTTP headers to use when making a multipart upload request include:

  • Content-Type. Set to multipart/related and include the boundary string you're using to identify the parts of the request.
  • Content-Length. Set to the total number of bytes in the request body.

The body of the request is formatted as a multipart/related content type [RFC2387] and contains exactly two parts. The parts are identified by a boundary string, and the final boundary string is followed by two hyphens.

Each part of the multipart request needs an additional Content-Type header:

  • Metadata part: Must come first, and Content-Type must match one of the the accepted metadata formats.
  • Media part: Must come second, and Content-Type must match one the method's accepted media MIME types.

Example

The following example shows the use of a multipart upload request for an upload path that would be /upload:

POST /upload?uploadType=multipart HTTP/1.1
Host: www.example.com
Content-Type: multipart/related; boundary="foo_bar_baz"
Content-Length: number_of_bytes_in_entire_request_body

--foo_bar_baz
Content-Type: application/json; charset=UTF-8

{
    "name": "Some value"
}

--foo_bar_baz
Content-Type: image/jpeg

JPEG data

--foo_bar_baz--

FormData upload way

This may be the most used way to upload files: it matches with the classic form "file" upload.

You just have to have a field of type file named file on your form and set the action path to /upload?uploadType=formData. It can ether be used with any XHR upload method.

Resumable upload way

To upload data files more reliably, you can use the resumable upload protocol. This protocol allows you to resume an upload operation after a communication failure has interrupted the flow of data. It is especially useful if you are transferring large files and the likelihood of a network interruption or some other transmission failure is high, for example, when uploading from a mobile client app. It can also reduce your bandwidth usage in the event of network failures because you don't have to restart large file uploads from the beginning.

The steps for using resumable upload include:

  1. Start a resumable session. Make an initial request to the upload URI that includes the metadata, if any.
  2. Save the resumable session URI. Save the session URI returned in the response of the initial request; you'll use it for the remaining requests in this session.
  3. Upload the file. Send the media file to the resumable session URI.

In addition, apps that use resumable upload need to have code to resume an interrupted upload. If an upload is interrupted, find out how much data was successfully received, and then resume the upload starting from that point.

Resumable Configuration

First, you need to configure the bundle.

Create your ResumableUploadSession entity

The entity will contains the resumable upload sessions and is required if you want the resumable way of upload to work.

<?php
namespace Acme\Entity;

use SRIO\RestUploadBundle\Entity\ResumableUploadSession as BaseResumableUploadSession;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class ResumableUploadSession extends BaseResumableUploadSession
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;
}

Copy this entity in your bundle and update the schema.

Configure the bundle

Configure the bundle to use this entity to store resumable upload sessions:

srio_rest_upload:
    resumable_entity_class: Acme\Entity\ResumableUploadSession

Resumable upload requests

Start a resumable session

For this initiating request, the body is either empty or it contains the metadata only; you'll transfer the actual contents of the file you want to upload in subsequent requests.

Use the following HTTP headers with the initial request:

  • X-Upload-Content-Type. Set to the media MIME type of the upload data to be transferred in subsequent requests.
  • X-Upload-Content-Length. Set to the number of bytes of upload data to be transferred in subsequent requests.
  • Content-Type. Set according to the metadata's data type.
  • Content-Length. Set to the number of bytes provided in the body of this initial request.

The following example shows the use of a resumable upload request for an upload path that would be /upload:

POST /upload?uploadType=resumable HTTP/1.1
Host: www.example.com
Content-Length: 41
Content-Type: application/json; charset=UTF-8
X-Upload-Content-Type: image/jpeg
X-Upload-Content-Length: 2000000

{
    "name": "Some value"
}

Save the resumable session URI

If the session initiation request succeeds, the API server responds with a 200 OK HTTP status code. In addition, it provides a Location header that specifies your resumable session URI. The Location header, shown in the example below, includes an uploadId query parameter portion that gives the unique upload ID to use for this session.

Here is the response to the request in the last step:

HTTP/1.1 200 OK
Location: /upload?uploadType=resumable&uploadId=fooBar123
Content-Length: 0

The value of the Location header, as shown in the above example response, is the session URI you'll use as the HTTP endpoint for doing the actual file upload or querying the upload status.

Upload the file

To upload the file, send a PUT request to the upload URI that you obtained in the previous step.

The HTTP headers to use when making the resumable file upload requests includes Content-Length. Set this to the number of bytes you are uploading in this request, which is generally the upload file size.

PUT /upload?uploadType=resumable&uploadId=fooBar123 HTTP/1.1
Content-Length: 2000000
Content-Type: image/jpeg

bytes 0-1999999

If the request succeeds, the server responds with an HTTP 201 Created, along with any metadata associated with this resource. If the initial request of the resumable session had been a PUT, to update an existing resource, the success response would be 200 OK, along with any metadata associated with this resource.

Upload file in chunks

With resumable uploads, you can break a file into chunks and send a series of requests to upload each chunk in sequence. This is not the preferred approach since there are performance costs associated with the additional requests, and it is generally not needed. However, you might need to use chunking to reduce the amount of data transferred in any single request.

If you are uploading the data in chunks, the Content-Range header is also required, along with the Content-Length header required for full file uploads:

  • Content-Length. Set to the chunk size or possibly less, as might be the case for the last request.
  • Content-Range: Set to show which bytes in the file you are uploading. For example, Content-Range: bytes 0-524287/2000000 shows that you are providing the first 524,288 bytes in a 2,000,000 byte file.

A sample request would be:

PUT {session_uri} HTTP/1.1
Host: www.example.com
Content-Length: 524288
Content-Type: image/jpeg
Content-Range: bytes 0-524287/2000000

bytes 0-524288

If the request succeeds, the server responds with 308 Resume Incomplete, along with a Range header that identifies the total number of bytes that have been stored so far:

HTTP/1.1 308 Resume Incomplete
Content-Length: 0
Range: 0-524287

Use the upper value returned in the Range header to determine where to start the next chunk. Continue to PUT each chunk of the file until the entire file has been uploaded

Resume an interrupted upload

If an upload request is terminated before receiving a response or if you receive an HTTP 503 Service Unavailable or even an HTTP 500 Internal Server Error response from the server, then you need to resume the interrupted upload. To do this:

Request the upload status

PUT {session_uri} HTTP/1.1
Content-Length: 0
Content-Range: bytes */2000000

Extract the number of bytes uploaded so far from the response The server's response uses the Range header to indicate that it has received the first 43 bytes of the file so far. Use the upper value of the Range header to determine where to start the resumed upload.

HTTP/1.1 308 Resume Incomplete
Content-Length: 0
Range: 0-42

Resume the upload from the point where it left off Use the upload file in chunks method to restart upload at the point where there's a failure.

Configuration reference

srio_rest_upload:
    # Define the available storages
    storages:
        name:
            # Filesystem service created by Gaufrette (or your own matching the Gaufrette's interface)
            filesystem: fs_service_id

            # Naming strategy service
            naming_strategy: srio_rest_upload.naming.default_strategy

            # Storage strategy service
            storage_strategy: srio_rest_upload.storage.default_strategy

    # The storage voter, that chose between storage based on upload context
    storage_voter: srio_rest_upload.storage_voter.default

    # The default storage name. With the default storage voter, it'll use
    # the first defined storage if value is null
    default_storage: ~

    # If you want to use the resumable upload way, you must set
    # the class name of your entity which store the upload sessions.
    resumable_entity_class: ~

    # Parameter the define the upload way, internally the provider selector
    upload_type_parameter: uploadType

The Advanced usage section explain the naming and storage strategies.

Testing

Tests are run with PHPUnit. Once you installed dependencies with composer, then:

  • Create a database, allow access to a user, and set configuration in Tests/Fixtures/App/app/config/parameters.yml file
  • Create the database schema for the test environment
php Tests/Fixtures/App/app/console doctrine:schema:update --force --env=test
  • Run PHPUnit
phpunit

Author: Sroze
Source Code: https://github.com/sroze/SRIORestUploadBundle 
License: 

#php #symfony 

A symfony bundle to handle multiple upload ways on your REST API

Serialized Response Bundle

SerializedResponseBundle

A simple bundle to provide an easy way to send out json/xml/yaml responses of serialized objects with annotations.

Introduction This Bundle allows you to directly return serializable objects as from within a controller to receive a serialized response in json/xml/yaml format. It requires jms/serializer-bundle.

Installation

Step 1 This library can be easily installed via composer

composer pulpmedia/serialized-response-bundle

or just add it to your composer.json file directly. This will install the pulpmedia/serialized-response-bundle and jms/serializer if not already installed.

Step 2 Enable the bundle in the kernel:

<?php
// app/AppKernel.php

public function registerBundles()
{
    $bundles = array(
        new JMS\SerializerBundle\JMSSerializerBundle(), //If you have not already done this already
        // ...
        new Pulpmedia\SerializedResponseBundle\PulpmediaSerializedResponseBundle(),
    );
}

Usage Using the Bundle is as easy as it comes. By returning the Object that should be serialized and using the corresponding annotation;

<?php
namespace My\Bundle\Controller

use Pulpmedia\SerializedResponseBundle\Annotation\SerializedResponse;

// ...

class MyController extents Controller{

  /**
   * @Route("/get/{id}")
   * @SerializedResponse(format="json/xml/yaml")
   */
  public function getAction($id){
    $em = $this->getDoctrine()->getManager();

    if($object = $em->getRepository('MyBundle:Entity')->find($id)){
        return $object;
    }
    return null;
  }
}

Author: Pulpmedia
Source Code: https://github.com/Pulpmedia/SerializedResponseBundle 
License: MIT License

#php #symfony 

Serialized Response Bundle

ResourceBundle: Symfony Bundle That Helps in Developing REST APIs

ResourceBundle

The ResourceBundle is an opinionated Symfony bundle to aid in developing REST APIs. It makes some architectural decisions for you, allowing you to focus more on the domain of your application. It uses as little magic as possible to make it easier to understand, debug, and extend.


Prerequisites The ResourceBundle relies on the FOSRestBundle to handle content negotiation and RESTful decoding of request bodies. After installing the bundle, you must configure it before proceeding to use the ResourceBundle. Here is a sample configuration to get started.

Note: The ResourceBundle does not handle any sort of authentication. It is meant to be used in conjunction with something like the FOSOAuthServerBundle.

Bundle Usage

##Resources The ResourceBundle is centered around resources. The bundle requires resources(entities) to implement the very simple ResourceInterface. The examples assume you're using Doctrine but the bundle is ORM agnostic. First, let's create a simple resource.

<?php // src/MyApp/CoreBundle/Entity/Task.php

namespace MyApp\CoreBundle\Entity;

use MyApp\CoreBundle\Domain\Task\TaskInterface;
use ProgrammingAreHard\ResourceBundle\Domain\ResourceInterface;

class Task implements TaskInterface, ResourceInterface
{
    protected $id;

    protected $task;

    public function getId()
    {
        return $this->id;
    }

    public function isNew()
    {
        return null === $this->getId();
    }

    public function getTask()
    {
        return $this->task;
    }

    public function setTask($task)
    {
        $this->task = $task;
    }
}

##Resource Repositories Once we have a resource, it's time to create a repository for the resource by implementing the ResourceRepositoryInterface. If using Doctrine, just extend the bundled BaseResourceRepository.

<?php // src/MyApp/CoreBundle/Domain/Task/Repository/Doctrine/TaskRepository.php

namespace MyApp\CoreBundle\Domain\Task\Repository\Doctrine;

use MyApp\CoreBundle\Domain\Task\Repository\TaskRepositoryInterface;
use MyApp\CoreBundle\Entity\Task;
use ProgrammingAreHard\ResourceBundle\Domain\Repository\Doctrine\BaseResourceRepository;

class TaskRepository extends BaseResourceRepository implements TaskRepositoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function newInstance()
    {
        return new Task;
    }
}

Register it with the container.

# src/MyApp/CoreBundle/Resources/services.yml

services:
    myapp.task.repository:
        class: Doctrine\ORM\EntityRepository
        factory_service: doctrine.orm.entity_manager
        factory_method: getRepository
        arguments:
            - MyApp\CoreBundle\Entity\Task

Remember to update the mapping file.

# src/MyApp/CoreBundle/Resources/doctrine/Task.orm.yml

MyApp\CoreBundle\Entity\Task:
    type: entity
    table: tasks
    repositoryClass: MyApp\CoreBundle\Domain\Task\Repository\Doctrine\TaskRepository
    id:
        id:
            type: integer
            generator:
                strategy: AUTO
    fields:
        task:
            type: string
            length: 255

##Resource Managers Just like Doctrine, persisting and deleting resources is not done by repositories. With the ResourceBundle, this is done through a ResourceManagerInterface implementation. If using Doctrine, you can use the bundled ResourceManager. Internally it uses Doctrine's ManagerRegistry to get the correct object manager for the resource.

# src/MyApp/CoreBundle/Resources/services.yml

services:

    # other services...

    myapp.resource.manager:
        class: ProgrammingAreHard\ResourceBundle\Domain\Manager\Doctrine\ResourceManager
        arguments:
            - @doctrine

Resource Forms

The bundle makes use of Symfony's form component to map incoming data to resources. Time to create a form for our Task.

<?php // src/MyApp/CoreBundle/Domain/Task/Form/Type/TaskType.php

namespace MyApp\CoreBundle\Domain\Task\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class TaskType extends AbstractType
{
    private $class;

    public function __construct($class)
    {
        $this->class = $class;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('task');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => $this->class,
        ));
    }

    public function getName()
    {
        return 'task';
    }
}

By default, the bundle attempts to find a resource's form by looking for a form with the name of the resource's class that has been lowercased and underscored. Ie. The bundle would expect to find a form by the name of todo_list for a MyApp/CoreBundle/Entity/TodoList resource.

Let's register the form with the container.

# src/MyApp/CoreBundle/Resources/services.yml

parameters:
    myapp.task.entity.class: MyApp\CoreBundle\Entity\Task

services:

    # other services...

    myapp.task.form.type:
        class: MyApp\CoreBundle\Domain\Task\Form\Type\TaskType
        arguments:
            - %myapp.task.entity.class%
        tags:
            - { name: form.type, alias: task }

Form Handlers

Now that we have a resource, repository, and form it's time to create an implementation of a FormHandlerInterface. Form handlers are only executed if a request was issued and the form was valid. The bundle comes with a SaveResourceFormHandler. It extracts the data(the resource from the form) and saves it through a ResourceManagerInterface. Let's register a resource form handler in the container.

# src/MyApp/CoreBundle/Resources/services.yml

services:

    # other services...

    myapp.resource.form_handler:
        class: ProgrammingAreHard\ResourceBundle\Domain\Form\Handler\SaveResourceFormHandler
        arguments:
            - @myapp.resource.manager

##Form Processors We need to use our new handler in a form processor.

# src/MyApp/CoreBundle/Resources/services.yml

services:

    # other services...

    myapp.resource.form_processor:
        class: ProgrammingAreHard\ResourceBundle\Domain\Form\FormProcessor
        arguments:
            - @myapp.resource.form_handler
            - @pah_resource.form.error_extractor

Form processors use a form handler if the form is valid and an error extractor for when it is invalid. If you do not like the default form error extractor, you can create your own by implementing the FormErrorExtractorInterface.

##Resource Controllers The glue that brings all these pieces together is the abstract ResourceController. Let's create a concrete TaskController.

<?php // src/MyApp/CoreBundle/Controller/TaskController.php

namespace MyApp\CoreBundle\Controller;

use MyApp\CoreBundle\Entity\Task;
use ProgrammingAreHard\ResourceBundle\Controller\ResourceController;
use ProgrammingAreHard\ResourceBundle\Domain\Event\ResourceEvents;
use ProgrammingAreHard\ResourceBundle\Domain\ResourceInterface;

class TaskController extends ResourceController
{
    /**
     * Task class.
     *
     * @var string
     */
    protected $resourceClass = Task::CLASS; // using php 5.5's class constant

    /**
     * Show current user's tasks.
     *
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function indexAction()
    {
        $tasks = $this->getUser()->getTasks();

        foreach ($tasks as $task) {
            $this->getResourceEventDispatcher()->dispatch(ResourceEvents::PRE_VIEW, $task);
        }

        return $this
            ->setData([$this->getPluralizedResourceName() => $tasks])
            ->respond();
    }

    /**
     * {@inheritdoc}
     */
    protected function getResourceLocation(ResourceInterface $resource)
    {
        return $this->generateUrl('my_app_task_view', ['id' => $resource->getId()]);
    }

    /**
     * {@inheritdoc}
     */
    protected function getFormProcessor()
    {
        return $this->get('myapp.resource.form_processor');
    }

    /**
     * {@inheritdoc}
     */
    protected function getResourceManager()
    {
        return $this->get('myapp.resource.manager');
    }

    /**
     * {@inheritdoc}
     */
    protected function getResourceRepository()
    {
        return $this->get('myapp.task.repository');
    }
}

Note: The $resourceClass is used by the ResourceController to find the relevant form, naming events, and serializing resources.

Tip: You might want to create your own base ResourceController and implement ::getFormProcessor() and ::getResourceManager() as they will probably be the same across each of your resource controllers.

Because the ResourceController uses symfony's security component to check basic REST permissions, we need to implement a security voter. You can customize this to suit your application's needs. For now, we're going to allow everything.

<?php //src/MyApp/CoreBundle/Domain/Resource/Security/Voter/ResourceVoter.php

namespace MyApp\CoreBundle\Domain\Resource\Security\Voter;

use ProgrammingAreHard\ResourceBundle\Domain\ResourceInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

class ResourceVoter implements VoterInterface
{
    /**
     * {@inheritdoc}
     */
    public function supportsAttribute($attribute)
    {
        return true
    }

    /**
     * {@inheritdoc}
     */
    public function supportsClass($class)
    {
        return true
    }

    /**
     * {@inheritdoc}
     */
    public function vote(TokenInterface $token, $resource, array $attributes)
    {
        // Your application's logic to determine if the user has permission
        // to perform 'VIEW', 'CREATE', 'UPDATE', and/or 'DELETE' permissions.

        if ($resource instanceof ResourceInterface) {
            return VoterInterface::ACCESS_GRANTED;
        }

        return VoterInterface::ACCESS_ABSTAIN;
    }
}

Don't forget to register it.

# src/MyApp/CoreBundle/Resources/services.yml

services:

    # other services...

    myapp.resource.security.voter:
        class: MyApp\CoreBundle\Domain\Resource\Security\Voter\ResourceVoter
        public: false
        tags:
            - { name: security.voter }

##Resource Routing Once we have our TaskController and our security voter set up, we can then RESTfully route to actions.

# app/config/routing.yml

# other routes...

myapp_tasks:
    resource: "@MyAppCoreBundle/Resources/config/routing.yml"
    prefix:   /api
# src/MyApp/CoreBundle/Resources/config/routing.yml

myapp_task_create:
    pattern:  /tasks
    defaults: { _controller: MyAppCoreBundle:Task:create }
    methods:  [POST]

myapp_task_view_all:
    pattern:  /tasks
    defaults: { _controller: MyAppCoreBundle:Task:index }
    methods:  [GET]

myapp_task_view:
    pattern:  /tasks/{id}
    defaults: { _controller: MyAppCoreBundle:Task:show }
    methods:  [GET]

myapp_task_update:
    pattern:  /tasks/{id}
    defaults: { _controller: MyAppCoreBundle:Task:update }
    methods:  [PUT]

myapp_task_delete:
    pattern:  /tasks/{id}
    defaults: { _controller: MyAppCoreBundle:Task:delete }
    methods:  [DELETE]

I highly recommend you take a peek at the ResourceController to see what's happening under the hood.

By default, the ResourceBundle uses Symfony's serializer component to serialize resources for responses. However, I recommend using the JMSSerializerBundle for more flexibility.

##Events

The bundle's components are developed in a manner to make it easy to add functionality. One important piece of functionality is the ability to dispatch events. Events can be dispatched by wrapping certain components in decorators. The bundle comes with three.

###Eventful Resource Manager You can use this decorator to dispatch events during manager interactions.

# src/MyApp/CoreBundle/Resources/services.yml

services:

    # other services...

    myapp.resource.eventful_manager:
        class: ProgrammingAreHard\ResourceBundle\Domain\Manager\Decorator\EventfulResourceManager
        arguments:
            - @myapp.resource.manager
            - @pah_resource.resource.event_dispatcher

By using this decorator, the following events will be dispatched:

  • task.pre_save
  • task.post_save
  • task.pre_create
  • task.post_create
  • task.pre_update
  • task.post_update
  • task.pre_delete
  • task.post_delete

It uses the ResourceEventDispatcher to dispatch these events. It uses the same class transformer as the ResourceController does when finding a resource's form. It lowercases and underscores a resource's class to use in the event name. Feel free to use the resource event dispatcher in your own code(like in your event listeners).

###Eventful Form Handler You can decorate your form handlers to dispatch pre and post handle events.

# src/MyApp/CoreBundle/Resources/services.yml

services:

    # other services...

    # redefine to use the eventful manager
    myapp.resource.form_handler:
        class: ProgrammingAreHard\ResourceBundle\Domain\Form\Handler\SaveResourceFormHandler
        arguments:
            - @myapp.resource.eventful_manager

    # redefine to use the above form handler
    myapp.resource.eventful_form_handler:
        class: ProgrammingAreHard\ResourceBundle\Domain\Form\Decorator\EventfulFormHandler
        arguments:
            - @myapp.resource.form_handler
            - @pah_resource.form.event_dispatcher

    # redefine to use the above eventful form handler
    myapp.resource.form_processor:
        class: ProgrammingAreHard\ResourceBundle\Domain\Form\FormProcessor
        arguments:
            - @myapp.resource.eventful_form_handler
            - @pah_resource.form.error_extractor

Using this decorator will dispatch the following events for the task's form handler:

  • task.form.pre_handle
  • task.form.post_handle

###Eventful Form Processor You can also decorate form processors to dispatch certain events throughout the form processing.

# src/MyApp/CoreBundle/Resources/services.yml

services:

    # other services...

    myapp.resource.eventful_form_processor:
        class: ProgrammingAreHard\ResourceBundle\Domain\Form\Decorator\EventfulFormProcessor
        arguments:
            - @myapp.resource.form_processor
            - @pah_resource.form.event_dispatcher

Using this decorator will dispatch the following events for the tasks's form.

  • task.form.initialize
  • task.form.invalid (only if the form is invalid)
  • task.form.complete (only if the form is valid)

To summarize, by taking advantage of these decorators you will have access to the following events:

  • task.pre_save
  • task.post_save
  • task.pre_create
  • task.post_create
  • task.pre_update
  • task.post_update
  • task.pre_delete
  • task.post_delete
  • task.form.initialize
  • task.form.invalid
  • task.form.pre_handle
  • task.form.post_handle
  • task.form.complete

Don't forget to use them in your ResourceControllers though!

##Bundle Configuration Reference This is the default bundle configuration.

# app/config/config.yml

programming_are_hard_resource:
    class_transformer: pah_resource.class_name.underscore_transformer
    form_error_extractor: pah_resource.form.flattened_error_extractor

The class_tranformer is responsible for turning a resource's fully qualified class name into a name it uses when finding a resource's form and dispatching events. It must implement TransformerInterface.

The form_error_extractor is responsbile for getting errors from a form. It must implement FormErrorExtractorInterface

Special Notes There are a few things to keep in mind when using this bundle.

IndexAction You might have noticed that there is no indexAction in the ResourceController. This is because the bundle can't reasonably guess all of the required parameters needed to come up with a generic enough solution. For example, most likely you will only want to display the current user's resources rather than everything in a resource repository. You may also want advanced url structures, ie. /api/people/24/tasks. The indexAction method signature we need to accommodate that person id wildcard. Consequently, pagination and filtering are left up to you.

ResourceEvents::PRE_VIEW This event is dispatched in the create, show, and update actions in the ResourceController. Depending on how you display related resources, you may want to ignore these events. You can use a serializer to display a resource's related resources. These resources would be fetched by calling getters on the resources themselves. Consequently, there is no place to dispatch the ResourceEvents::PRE_VIEW events for the related resources and still keep the bundle ORM agnostic. One option is to pull in the BazingaHateoasBundle and configure links for related resources. This way you can be sure that individual resources are only ever displayed directly by the ResourceController.

Author: ProgrammingAreHard
Source Code: https://github.com/ProgrammingAreHard/ResourceBundle 
License: MIT License

#php #symfony 

ResourceBundle: Symfony Bundle That Helps in Developing REST APIs

Add Rate Limits to Your Controllers/actions Easily Through annotations

NoxlogicRateLimitBundle

This bundle provides enables the @RateLimit annotation which allows you to limit the number of connections to actions. This is mostly useful in APIs.

The bundle is prepared to work by default in cooperation with the FOSOAuthServerBundle. It contains a listener that adds the OAuth token to the cache-key. However, you can create your own key generator to allow custom rate limiting based on the request. See Create a custom key generator below.

Features

  • Simple usage through annotations
  • Customize rates per controller, action and even per HTTP method
  • Multiple storage backends: Redis, Memcached and Doctrine cache

Installation

Installation takes just few easy steps:

Step 1: Add the bundle to your composer.json

If you're not yet familiar with Composer see http://getcomposer.org. Add the NoxlogicRateLimitBundle in your composer.json:

{
    "require": {
        "noxlogic/ratelimit-bundle": "1.x"
    }
}

Now tell composer to download the bundle by running the command:

php composer.phar update noxlogic/ratelimit-bundle

Step 2: Enable the bundle

Enable the bundle in the kernel:

<?php
// app/AppKernel.php

public function registerBundles()
{
    $bundles = array(
        // ...
        new Noxlogic\RateLimitBundle\NoxlogicRateLimitBundle(),
    );
}

Step 3: Install a storage engine

Redis

If you want to use Redis as your storage engine, you might want to install SncRedisBundle:

Memcache

If you want to use Memcache, you might want to install LswMemcacheBundle

Doctrine cache

If you want to use Doctrine cache as your storage engine, you might want to install DoctrineCacheBundle:

Referer to their documentations for more details. You can change your storage engine with the storage_engine configuration parameter. See Configuration reference.

Configuration

Enable bundle only in production

If you wish to enable the bundle only in production environment (so you can test without worrying about limit in your development environments), you can use the enabled configuration setting to enable/disable the bundle completely. It's enabled by default:

# config_dev.yml
noxlogic_rate_limit:
    enabled: false

Configuration reference

This is the default bundle configuration:

noxlogic_rate_limit:
    enabled:              true

    # The storage engine where all the rates will be stored
    storage_engine:       ~ # One of "redis"; "memcache"; "doctrine"; "php_redis"; "php_redis_cluster"

    # The redis client to use for the redis storage engine
    redis_client:         default_client
    
    # The Redis service, use this if you dont use SncRedisBundle and want to specify a service to use
    # Should be instance of \Predis\Client
    redis_service:    null # Example: project.predis

    # The Redis client to use for the php_redis storage engine
    # Depending on storage_engine an instance of \Redis or \RedisCluster
    php_redis_service:    null # Example: project.redis

    # The memcache client to use for the memcache storage engine
    memcache_client:      default
    
    # The Memcached service, use this if you dont use LswMemcacheBundle and want to specify a service to use
    # Should be instance of \Memcached
    memcache_service:    null # Example: project.memcached

    # The Doctrine Cache provider to use for the doctrine storage engine
    doctrine_provider:    null # Example: my_apc_cache
    
    # The Doctrine Cache service, use this if you dont use DoctrineCacheBundle and want to specify a service to use
    # Should be an instance of \Doctrine\Common\Cache\Cache
    doctrine_service:    null # Example: project.my_apc_cache

    # The HTTP status code to return when a client hits the rate limit
    rate_response_code:   429

    # Optional exception class that will be returned when a client hits the rate limit
    rate_response_exception:  null

    # The HTTP message to return when a client hits the rate limit
    rate_response_message:  'You exceeded the rate limit'

    # Should the ratelimit headers be automatically added to the response?
    display_headers:      true

    # What are the different header names to add
    headers:
        limit:                X-RateLimit-Limit
        remaining:            X-RateLimit-Remaining
        reset:                X-RateLimit-Reset

    # Rate limits for paths
    path_limits:
        path:                 ~ # Required
        methods:

            # Default:
            - *
        limit:                ~ # Required
        period:               ~ # Required
        
    # - { path: /api, limit: 1000, period: 3600 }
    # - { path: /dashboard, limit: 100, period: 3600, methods: ['GET', 'POST']}

    # Should the FOS OAuthServerBundle listener be enabled 
    fos_oauth_key_listener: true

Usage

Simple rate limiting

To enable rate limiting, you only need to add the annotation to the docblock of the specified action

<?php

use Noxlogic\RateLimitBundle\Annotation\RateLimit;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route(...)
 *
 * @RateLimit(limit=1000, period=3600)
 */
public function someApiAction()
{
}

Limit per method

It's possible to rate-limit specific HTTP methods as well. This can be either a string or an array of methods. When no method argument is given, all other methods not defined are rated. This allows to add a default rate limit if needed.

<?php

use Noxlogic\RateLimitBundle\Annotation\RateLimit;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route(...)
 *
 * @RateLimit(methods={"PUT", "POST"}, limit=1000, period=3600)
 * @RateLimit(methods={"GET"}, limit=1000, period=3600)
 * @RateLimit(limit=5000, period=3600)
 */
public function someApiAction()
{
}

Limit per controller

It's also possible to add rate-limits to a controller class instead of a single action. This will act as a default rate limit for all actions, except the ones that actually defines a custom rate-limit.

<?php

use Noxlogic\RateLimitBundle\Annotation\RateLimit;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Ratelimit(methods={"POST"}, limit=100, period=10); // 100 POST requests per 10 seconds
 */
class DefaultController extends Controller
{
    /**
     * @Ratelimit(method="POST", limit=200, period=10); // 200 POST requests to indexAction allowed.
     */
    public function indexAction()
    {
    }
}

Create a custom key generator

NOTE

Note that this bundle by default does not perform rate-limiting based on user's IP. If you wish to enable IP-based rate limiting or any other strategy, custom key generators are the way to go.

If you need to create a custom key generator, you need to register a listener to listen to the ratelimit.generate.key event:

services:
    mybundle.listener.rate_limit_generate_key:
        class: MyBundle\Listener\RateLimitGenerateKeyListener
        tags:
            - { name: kernel.event_listener, event: 'ratelimit.generate.key', method: 'onGenerateKey' }
<?php

namespace MyBundle\Listener;

use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;

class RateLimitGenerateKeyListener
{
    public function onGenerateKey(GenerateKeyEvent $event)
    {
        $key = $this->generateKey();

        $event->addToKey($key);
        // $event->setKey($key); // to overwrite key completely
    }
}

Make sure to generate a key based on what is rate limited in your controllers.

And example of a IP-based key generator can be:

<?php

namespace MyBundle\Listener;

use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;

class IpBasedRateLimitGenerateKeyListener
{
    public function onGenerateKey(GenerateKeyEvent $event)
    {
        $request = $event->getRequest();
        $event->addToKey($request->getClientIp());
    }
}

Throwing exceptions

Instead of returning a Response object when a rate limit has exceeded, it's also possible to throw an exception. This allows you to easily handle the rate limit on another level, for instance by capturing the kernel.exception event.

Running tests

If you want to run the tests use:

./vendor/bin/phpunit ./Tests

This bundle is partially inspired by a GitHub gist from Ruud Kamphuis: https://gist.github.com/ruudk/3350405

Author: jaytaph
Source Code: https://github.com/jaytaph/RateLimitBundle 
License: MIT License

#php #symfony 

Add Rate Limits to Your Controllers/actions Easily Through annotations

LexikJWTAuthenticationBundle: JWT Authentication for Your Symfony API

LexikJWTAuthenticationBundle

This bundle provides JWT (Json Web Token) authentication for your Symfony API.

It is compatible (and tested) with PHP 7.1+ on Symfony 4.x, 5.x and 6.x.

Getting started

Prerequisites

This bundle requires Symfony 4.4+ and the openssl extension.

Protip: Though the bundle doesn't enforce you to do so, it is highly recommended to use HTTPS.

Installation

Add lexik/jwt-authentication-bundle to your composer.json file:

$ php composer.phar require "lexik/jwt-authentication-bundle"

Register the bundle

Register bundle into config/bundles.php (Flex did it automatically):

return [
    //...
    Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
];

Generate the SSL keys

$ php bin/console lexik:jwt:generate-keypair

Your keys will land in config/jwt/private.pem and config/jwt/public.pem (unless you configured a different path).

Available options:

  • --skip-if-exists will silently do nothing if keys already exist.
  • --overwrite will overwrite your keys if they already exist.

Otherwise, an error will be raised to prevent you from overwriting your keys accidentally.

Configuration

Configure the SSL keys path and passphrase in your .env:

JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%' # required for token creation
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%' # required for token verification
    pass_phrase: '%env(JWT_PASSPHRASE)%' # required for token creation
    token_ttl: 3600 # in seconds, default is 3600

Configure application security

Caution!

Make sure the firewall login is place before api, and if main exists, put it after api, otherwise you will encounter /api/login_check route not found.

Symfony versions prior to 5.3

# config/packages/security.yaml
security:
    # ...

    firewalls:
        login:
            pattern: ~/api/login
            stateless: true
            json_login:
                check_path: /api/login_check # or api_login_check as defined in config/routes.yaml
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        api:
            pattern:   ~/api
            stateless: true
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator

    access_control:
        - { path: ~/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ~/api,       roles: IS_AUTHENTICATED_FULLY }

Symfony 5.3 and higher

# config/packages/security.yaml
security:
    enable_authenticator_manager: true
    # ...

    firewalls:
        login:
            pattern: ~/api/login
            stateless: true
            json_login:
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        api:
            pattern:   ~/api
            stateless: true
            jwt: ~

    access_control:
        - { path: ~/api/login, roles: PUBLIC_ACCESS }
        - { path: ~/api,       roles: IS_AUTHENTICATED_FULLY }

Configure application routing

# config/routes.yaml
api_login_check:
    path: /api/login_check

Usage

1. Obtain the token

The first step is to authenticate the user using its credentials. You can test getting the token with a simple curl command like this (adapt host and port):

Linux or macOS:

$ curl -X POST -H "Content-Type: application/json" https://localhost/api/login_check -d '{"username":"johndoe","password":"test"}'

Windows:

C:\> curl -X POST -H "Content-Type: application/json" https://localhost/api/login_check --data {\"username\":\"johndoe\",\"password\":\"test\"}

If it works, you will receive something like this:

{
    "token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE0MzQ3Mjc1MzYsInVzZXJuYW1lIjoia29ybGVvbiIsImlhdCI6IjE0MzQ2NDExMzYifQ.nh0L_wuJy6ZKIQWh6OrW5hdLkviTs1_bau2GqYdDCB0Yqy_RplkFghsuqMpsFls8zKEErdX5TYCOR7muX0aQvQxGQ4mpBkvMDhJ4-pE4ct2obeMTr_s4X8nC00rBYPofrOONUOR4utbzvbd4d2xT_tj4TdR_0tsr91Y7VskCRFnoXAnNT-qQb7ci7HIBTbutb9zVStOFejrb4aLbr7Fl4byeIEYgp2Gd7gY"
}

Store it (client side), the JWT is reusable until its TTL has expired (3600 seconds by default).

2. Use the token

Simply pass the JWT on each request to the protected firewall, either as an authorization header or as a query parameter.

By default only the authorization header mode is enabled : Authorization: Bearer {token}

See the :doc:`configuration reference </1-configuration-reference>` document to enable query string parameter mode or change the header value prefix.

Examples

See :doc:`Functionally testing a JWT protected api </3-functional-testing>` document or the sandbox application Symfony4) for a fully working example.

Notes

About token expiration

Each request after token expiration will result in a 401 response. Redo the authentication process to obtain a new token.

Maybe you want to use a refresh token to renew your JWT. In this case you can check JWTRefreshTokenBundle.

Working with CORS requests

This is more of a Symfony2 related topic, but see :doc:`Working with CORS requests </4-cors-requests>` document to get a quick explanation on handling CORS requests.

Impersonation

For impersonating users using JWT, see https://symfony.com/doc/current/security/impersonating_user.html

Important note for Apache users

As stated in this link and this one, Apache server will strip any Authorization header not in a valid HTTP BASIC AUTH format.

If you intend to use the authorization header mode of this bundle (and you should), please add those rules to your VirtualHost configuration :

SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1

Further documentation

The following documents are available:

Community Support

Please consider opening a question on StackOverflow using the lexikjwtauthbundle tag, it is the official support platform for this bundle.

Github Issues are dedicated to bug reports and feature requests.

Contributing

See the CONTRIBUTING file.

Upgrading from 1.x

Please see the UPGRADE file.

Credits

Author: Lexik
Source Code: https://github.com/lexik/LexikJWTAuthenticationBundle 
License: MIT License

#php #symfony #jwt 

LexikJWTAuthenticationBundle: JWT Authentication for Your Symfony API

This Bundle Provides REST Endpoints for Doctrine Entities

LemonRestBundle

This bundle provides REST endpoints for Doctrine entities. It is a highly opinionated, convention driven solution to rapidly deploying a REST API. It relies on the strengths of libraries like JMS Serializer, Will Durands Content Negotiation library and the Doctrine Object Relation Mapper library to make assumptions and drive your REST API off of convention.

If you are looking for an extremely flexible way to build REST APIs this library is not for you, and I suggest you take a look at the fantastic work being done on the Friends of Symfony REST Bundle.

Setting up the bundle

  1. Add LemonRestBundle via composer:
composer require stanlemon/rest-bundle

or

{
   "require": {
       "stanlemon/rest-bundle": "dev-master@dev"
   }
}
  1. Register the bundle in your application:
// app/AppKernel.php
class AppKernel extends Kernel
{
    // ...
    public function registerBundles()
    {
        $bundles = array(
            // ...
            new JMS\SerializerBundle\JMSSerializerBundle(),
            new Lemon\RestBundle\LemonRestBundle()
        );
    }
}
  1. Add routing to your routing.yml:
lemon_rest:
    resource: "@LemonRestBundle/Resources/config/routing.yml"
    prefix:   /api

Adding support for your Doctrine entities

There are currently three ways you can add entities to be used as REST resources.

  1. Use the annotation
// src/Lemon/TestBundle/Entity/Person.php

namespace Lemon\TestBundle\Entity;;

use Doctrine\ORM\Mapping as ORM;
use Lemon\RestBundle\Annotation as Rest;
use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;

/**
 * @ORM\Table()
 * @ORM\Entity()
 * @Rest\Resource(name="person")
 */
class Person
{
    /**
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    public $id;

    /**
     * @ORM\Column(name="name", type="string", length=255, nullable=false)
     * @Assert\NotBlank()
     */
    public $name;
 }
  1. Use the object registry, retrieve the lemon_rest.object_registry service from the dependency injection container and then
$objectRegistry->addClass('person', 'Lemon\TestBundle\Entity\Person');
  1. Add explicit configuration to your app
lemon_rest:
    mappings:
        - { name: post, class: Lemon\RestDemoBundle\Entity\Post }
        - { name: comment, class: Lemon\RestDemoBundle\Entity\Comment }
        - { name: tag, class: Lemon\RestDemoBundle\Entity\Tag }
The 'name' refers to the resource, specifically the portion of the endpoint uri that refers to the object. The 'class' should be the fully qualified namespace path of the Doctrine Entity you wish to add to the object registry.

Running the tests

After installing dependencies with composer (including require-dev) simply Run phpunit

$ ./vendor/bin/phpunit -c ./phpunit.xml

The RestControllerTest is a functional test that show cases many of the ways which this bundle can be used.

Serialization & Deserialization

You can custom the serialize/deserialize process of your entities using JMS Serializer, please reference the documentation for specifics, such as accessor methods and exclusions.

 use JMS\Serializer\Annotation as Serializer;
 
 class Person
 {
     /**
      * @Serializer\Exclude()
      */
     public $ssn;
 }

Validation

The REST bundle uses the Symfony Validation component to validate entities on POST and PUT operations. This means that you can easily add validation rules to your REST api by simply annotating your entity (or through yaml/xml configuration).

use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    /**
     * @Assert\NotBlank()
     */
    public $name;
}

Events

There are several points at which you can tie into the bundle, the following events are available using the event dispatcher

- lemon_rest.event.pre_search
- lemon_rest.event.post_search
- lemon_rest.event.pre_create
- lemon_rest.event.post_create
- lemon_rest.event.pre_retrieve
- lemon_rest.event.post_retrieve
- lemon_rest.event.pre_update
- lemon_rest.event.post_update
- lemon_rest.event.pre_delete
- lemon_rest.event.post_delete

You can register event listeners and subscribers simply by tagging your service definitions

lemon_rest.event_listener
lemon_rest.event_subscriber

Envelopes

The bundle uses an Envelope object to return the final payload to the serializer. This envelope can be customized so long as it implements the Lemon\RestBundle\Object\Envelope interface. A default envelope is provided, as well as an envelope that flattens the search results output, this is particularly helpful when using a framework like Restangular. Envelopes are a good way to customize the bundle's output to cater to the needs of your particular consuming client.

To switch to the FlattenedEnvelope (or any custom envelope of your choosing) you would add the following in your app config

lemon_rest:
    envelope: Lemon\RestBundle\Object\Envelope\FlattenedEnvelope

Criteria

The bundle uses an Criteria object to manage search criteria that gets passed to the ObjectManager. This criteria object specifically filters out reserved terms from a request object, such as _orderBy _limit _offset and _orderDir. The default behavior, and more specifically the default fields mentioned may not be exactly what you want for your project. That's ok because you can customize the Criteria object that is used. When you create your custom Criteria object the only requirement is that it implements the Lemon\RestBundle\Object\Criteria interface.

To switch to a different Criteria object create a class implementing Lemon\RestBundle\Object\Criteria and add the following to your application's semantic configuration

lemon_rest:
    criteria: Acme\DemoAppBundle\Rest\MyCriteria

Documentation

Documentation is stored in the Resources/doc/index.md file in this bundle:

Demo

A demo application is available at https://github.com/stanlemon/rest-demo-app

You can also try it yourself online at http://restdemo-stanlemon.rhcloud.com

Installation

All the installation instructions are located in the documentation.

Changes

Details about changes from version to version are stored in CHANGELOG.md

Reporting an issue or a feature request

Issues and feature requests are tracked in the Github issue tracker.

Contributing

Go for it! Please open a PR against the develop branch and make sure to, at the very least, include a functional test covering your work. Or even better, full functional and unit tests!

Author: Stanlemon
Source Code: https://github.com/stanlemon/rest-bundle 
License: MIT License

#php #symfony 

This Bundle Provides REST Endpoints for Doctrine Entities

Symfony JSON-API Transformer Bundle

Symfony JSON-API Transformer Bundle

For Symfony 2 and Symfony 3

Installation

Step 1: Download the Bundle

Open a command console, enter your project directory and execute the following command to download the latest stable version of this bundle:

$ composer require nilportugues/jsonapi-bundle

Step 2: Enable the Bundle

Then, enable the bundle by adding it to the list of registered bundles in the app/AppKernel.php file of your project:

<?php
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...
            new NilPortugues\Symfony\JsonApiBundle\NilPortuguesSymfonyJsonApiBundle(),
        );
        // ...
    }
    // ...
}

Usage

Creating the mappings

Mapping directory

Mapping files should be located at the app/config/serializer directory. This directory must be created.

It can be also be customized and placed elsewhere by editing the app/config/config.yml configuration file:

# app/config/config.yml

nilportugues_json_api:
    mappings: 
        - "%kernel.root_dir%/config/serializer/"
        - @AppBundle/Product/config/Mappings

Mapping files

The JSON-API transformer works by transforming an existing PHP object into its JSON representation. For each object, a mapping file is required.

Mapping files must be placed in the mappings directory. The expected mapping file format is .yml and will allow you to rename, hide and create links relating all of your data.

For instance, here's a quite complex Post object to demonstrate how it works:

$post = new Post(
    new PostId(9),
    'Hello World',
    'Your first post',
    new User(
        new UserId(1),
        'Post Author'
    ),
    [
        new Comment(
            new CommentId(1000),
            'Have no fear, sers, your king is safe.',
            new User(new UserId(2), 'Barristan Selmy'),
            [
                'created_at' => (new DateTime('2015/07/18 12:13:00'))->format('c'),
                'accepted_at' => (new DateTime('2015/07/19 00:00:00'))->format('c'),
            ]
        ),
    ]
);

And the series of mapping files required:

# app/config/serializer/acme_domain_dummy_post.yml

mapping:
  class: Acme\Domain\Dummy\Post
  alias: Message
  aliased_properties:
    author: author
    title: headline
    content: body
  hide_properties: []
  id_properties:
    - postId
  urls:
    self: get_post ## @Route name
    comments: get_post_comments ## @Route name
  relationships:
    author:
      related: get_post_author ## @Route name
      self: get_post_author_relationship  ## @Route name
# app/config/serializer/acme_domain_dummy_value_object_post_id.yml

mapping:
  class: Acme\Domain\Dummy\ValueObject\PostId
  aliased_properties: []
  hide_properties: []
  id_properties:
  - postId
  urls:
    self: get_post  ## @Route name
  relationships:
    comment:
      self: get_post_comments_relationship  ## @Route name
# app/config/serializer/acme_domain_dummy_comment.yml

mapping:
  class: Acme\Domain\Dummy\Comment
  aliased_properties: []
  hide_properties: []
  id_properties:
    - commentId
  urls:
    self: get_comment ## @Route name
  relationships:
    post:
      self: get_post_comments_relationship ## @Route name
# app/config/serializer/acme_domain_dummy_value_object_comment_id.yml

mapping:
  class: Acme\Domain\Dummy\ValueObject\CommentId
  aliased_properties: []
  hide_properties: []
  id_properties:
    - commentId
  urls:
    self: get_comment ## @Route name
  relationships:
    post:
      self: get_post_comments_relationship ## @Route name
# app/config/serializer/acme_domain_dummy_user.yml

mapping:
  class: Acme\Domain\Dummy\User
  aliased_properties: []
  hide_properties: []
  id_properties:
  - userId
  urls:
    self: get_user
    friends: get_user_friends  ## @Route name
    comments: get_user_comments  ## @Route name
# app/config/serializer/acme_domain_dummy_value_object_user_id.yml

mapping:
  class: Acme\Domain\Dummy\ValueObject\UserId
  aliased_properties: []
  hide_properties: []
  id_properties:
  - userId
  urls:
    self: get_user  ## @Route name
    friends: get_user_friends  ## @Route name
    comments: get_user_comments  ## @Route name

Outputing API Responses

It is really easy, just get an instance of the JsonApiSerializer from the Service Container and pass the object to its serialize() method. Output will be valid JSON-API.

Here's an example of a Post object being fetched from a Doctrine repository.

Finally, a helper trait, JsonApiResponseTrait is provided to write fully compilant responses wrapping the PSR-7 Response objects provided by the original JSON API Transformer library.

<?php
namespace AppBundle\Controller;

use NilPortugues\Symfony\JsonApiBundle\Serializer\JsonApiResponseTrait;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class PostController extends Controller
{
    use JsonApiResponseTrait;

    /**
     * @\Symfony\Component\Routing\Annotation\Route("/post/{postId}", name="get_post")
     *
     * @param $postId
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function getPostAction($postId)
    {
        $post = $this->get('doctrine.post_repository')->find($postId);
        
        $serializer = $this->get('nil_portugues.serializer.json_api_serializer');

        /** @var \NilPortugues\Api\JsonApi\JsonApiTransformer $transformer */
        $transformer = $serializer->getTransformer();
        $transformer->setSelfUrl($this->generateUrl('get_post', ['postId' => $postId], true));
        $transformer->setNextUrl($this->generateUrl('get_post', ['postId' => $postId+1], true));

        return $this->response($serializer->serialize($post));
    }
} 

Output:

HTTP/1.1 200 OK
Cache-Control: private, max-age=0, must-revalidate
Content-type: application/vnd.api+json
{
    "data": {
        "type": "message",
        "id": "9",
        "attributes": {
            "headline": "Hello World",
            "body": "Your first post"
        },
        "links": {
            "self": {
                "href": "http://example.com/posts/9"
            },
            "comments": {
                "href": "http://example.com/posts/9/comments"
            }
        },
        "relationships": {
            "author": {
                "links": {
                    "self": {
                        "href": "http://example.com/posts/9/relationships/author"
                    },
                    "related": {
                        "href": "http://example.com/posts/9/author"
                    }
                },
                "data": {
                    "type": "user",
                    "id": "1"
                }
            }
        }
    },
    "included": [
        {
            "type": "user",
            "id": "1",
            "attributes": {
                "name": "Post Author"
            },
            "links": {
                "self": {
                    "href": "http://example.com/users/1"
                },
                "friends": {
                    "href": "http://example.com/users/1/friends"
                },
                "comments": {
                    "href": "http://example.com/users/1/comments"
                }
            }
        },
        {
            "type": "user",
            "id": "2",
            "attributes": {
                "name": "Barristan Selmy"
            },
            "links": {
                "self": {
                    "href": "http://example.com/users/2"
                },
                "friends": {
                    "href": "http://example.com/users/2/friends"
                },
                "comments": {
                    "href": "http://example.com/users/2/comments"
                }
            }
        },
        {
            "type": "comment",
            "id": "1000",
            "attributes": {
                "dates": {
                    "created_at": "2015-08-13T21:11:07+02:00",
                    "accepted_at": "2015-08-13T21:46:07+02:00"
                },
                "comment": "Have no fear, sers, your king is safe."
            },
            "relationships": {
                "user": {
                    "data": {
                        "type": "user",
                        "id": "2"
                    }
                }
            },            
            "links": {
                "self": {
                    "href": "http://example.com/comments/1000"
                }
            }
        }
    ],
    "links": {
        "self": {
            "href": "http://example.com/posts/9"
        },
        "next": {
            "href": "http://example.com/posts/10"
        }
    },
    "jsonapi": {
        "version": "1.0"
    }
}

Request objects

JSON API comes with a helper Request class, NilPortugues\Api\JsonApi\Http\Request\Request(ServerRequestInterface $request), implementing the PSR-7 Request Interface. Using this request object will provide you access to all the interactions expected in a JSON API:

JSON API Query Parameters:

  • &fields[resource]=field1,field2 will only show the specified fields for a given resource.
  • &include=resource show the relationship for a given resource.
  • &include=resource.resource2 show the relationship field for those depending on resource2.
  • &sort=field1,-field2 sort by field2 as DESC and field1 as ASC.
  • &sort=-field1,field2 sort by field1 as DESC and field2 as ASC.
  • &page[number] will return the current page elements in a page-based pagination strategy.
  • &page[size] will return the total amout of elements in a page-based pagination strategy.
  • &page[limit] will return the limit in a offset-based pagination strategy.
  • &page[offset] will return the offset value in a offset-based pagination strategy.
  • &page[cursor] will return the cursor value in a cursor-based pagination strategy.
  • &filter will return data passed in the filter param.

NilPortugues\Api\JsonApi\Http\Request\Request

Given the query parameters listed above, Request implements helper methods that parse and return data already prepared.

namespace \NilPortugues\Api\JsonApi\Http\Request;

class Request
{
  public function __construct(ServerRequestInterface $request = null) { ... }
  public function getIncludedRelationships() { ... }
  public function getSort() { ... }
  public function getPage() { ... }
  public function getFilters() { ... }
  public function getFields() { ... }
}

Response objects (JsonApiResponseTrait)

The following JsonApiResponseTrait methods are provided to return the right headers and HTTP status codes are available:

    private function errorResponse($json);
    private function resourceCreatedResponse($json);
    private function resourceDeletedResponse($json);
    private function resourceNotFoundResponse($json);
    private function resourcePatchErrorResponse($json);
    private function resourcePostErrorResponse($json);
    private function resourceProcessingResponse($json);
    private function resourceUpdatedResponse($json);
    private function response($json);
    private function unsupportedActionResponse($json);

Integration with NelmioApiDocBundleBundle

The NelmioApiDocBundle is a very well known bundle used to document APIs. Integration with the current bundle is terrible easy.

Here's an example following the PostContoller::getPostAction() provided before:

<?php
namespace AppBundle\Controller;

use NilPortugues\Symfony\JsonApiBundle\Serializer\JsonApiResponseTrait;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class PostController extends Controller
{
    use JsonApiResponseTrait;

    /**
     * Get a Post by its identifier. Will return Post, Comments and User data.
     *
     * @Nelmio\ApiDocBundle\Annotation\ApiDoc(
     *  resource=true,
     *  description="Get a Post by its unique id",
     * )
     *
     * @Symfony\Component\Routing\Annotation\Route("/post/{postId}", name="get_post")
     * @Sensio\Bundle\FrameworkExtraBundle\Configuration\Method({"GET"})
     *
     * @param $postId
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function getPostAction($postId)
    {
        $post = $this->get('doctrine.post_repository')->find($postId);
        
        $serializer = $this->get('nil_portugues.serializer.json_api_serializer');

        /** @var \NilPortugues\Api\JsonApi\JsonApiTransformer $transformer */
        $transformer = $serializer->getTransformer();
        $transformer->setSelfUrl($this->generateUrl('get_post', ['postId' => $postId], true));
        $transformer->setNextUrl($this->generateUrl('get_post', ['postId' => $postId+1], true));

        return $this->response($serializer->serialize($post));
    }
} 

And the recommended configuration to be added in app/config/config.yml

#app/config/config.yml

nelmio_api_doc:
  sandbox:
        authentication:
          name: access_token
          delivery: http
          type:     basic
          custom_endpoint: false
        enabled:  true
        endpoint: ~
        accept_type: ~
        body_format:
            formats: []
            default_format: form
        request_format:
            formats:
                json: application/vnd.api+json
            method: accept_header
            default_format: json
        entity_to_choice: false

Quality

To run the PHPUnit tests at the command line, go to the tests directory and issue phpunit.

This library attempts to comply with PSR-1, PSR-2, PSR-4 and PSR-7.

If you notice compliance oversights, please send a patch via Pull Request.

Contribute

Contributions to the package are always welcome!

Support

Get in touch with me using one of the following means:

Authors

Author: Nilportugues
Source Code: https://github.com/nilportugues/symfony-jsonapi 
License: MIT License

#php #symfony #api #json 

Symfony JSON-API Transformer Bundle

JMDRestBundle: Symfony 2 Bundle Allows You to Fast Generate REST Api

JMDRestBundle

This bundle provide fast and simple way to generate REST api for your project entities without editing configs and creating any controllers.

Feautures:

  • CRUD web api
  • Independent from other bundles and do not required bundles like FOSRestBundle or JMSSerializerBundle, etc..
  • Built-in pagination and ordering

Installation

  • Download via composer
$ composer require jmd/rest-bundle
  • Add into app/AppKernel.php: ```php public function registerBundles() { $bundles = array( ... new JMD\RestBundle\JMDRestBundle(), ... ); ... return $bundles; }
* Add into `app/config/routing.yml`:
```yaml
jmd_rest:
    resource: "@JMDRestBundle/Controller/RestController.php"
    type:     annotation
    prefix:   /api

Usage

Update, delete and add methods you can use as is after installation.

Api routes:

Route parameters:

  1. bundleName - name of entity bundle
  2. entityName - name of entity
  3. id - entity item id
namemethodpathcomment
rest_get_entity_listGET/api/{bundleName}/{entityName}Show list entity items
rest_get_entity_itemGET/api/{bundleName}/{entityName}/{id}Show entity item by id
rest_update_entity_itemPUT/api/{bundleName}/{entityName}/{id}Update entity item by id
rest_x_update_entity_itemPUT/api/{bundleName}/{entityName}/{id}/xSpecial update action for x-editable jQuery plugin
rest_delete_entity_itemDELETE/api/{bundleName}/{entityName}/{id}Delete entity item id
rest_entity_add_itemPOST/api/{bundleName}/{entityName}Add new entity item

How to add or update item:

Request headers must have Content-Type equals application/json. For update any field in entity we must construct there json structure:

{
    "fieldName": "value",
    "fieldName2": "value"
}

Updating and posting supports relations. To save relations we have to set json like:

{
    "relationFieldToMany": [id1,id2],
    "relationFieldToOne": id3
}

How to show item or items:

For showing item in entity repository we must implement \JMD\RestBundle\Entity\RestEntityInterface and make methods:

  • findAllArray(array $order = []) - must return query builder. Example: ```php public function findAllArray(array $order = []) { $qb = $this->createQueryBuilder('c'); $qb->select('partial c.{id,name}'); return $qb;

}

* `findOneArray($id)` - must return array or null result. Example:
```php
public function findOneArray($id)
{
        $qb = $this->createQueryBuilder('c');

        $qb
                ->select('partial c.{id,name}')
                ->where('c.id = :id')
                ->setParameter('id', $id)
        ;

        return $qb->getQuery()->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY);
}

After you make implementation, you can send GET request and will get result like this:

# url: http://localhost/api/BundleName/Client
{
    "status": 200,
    "data": [
        {
            "id": 1,
            "name": "Test client"
        },
        {
            "id": 2,
            "name": "Test client 2"
        }
    ]
}

# url: http://localhost/api/BundleName/Client/1
{
    "status": 200,
    "data":  {
        "id": 1,
        "name": "Test client"
    }
}

That's all!

Author: mops1k
Source Code: https://github.com/mops1k/JMDRestBundle 
License: 

#php #symfony 

JMDRestBundle: Symfony 2 Bundle Allows You to Fast Generate REST Api

Provides Various tools To Rapidly Develop RESTful API's with Symfony

FOSRestBundle

This bundle provides various tools to rapidly develop RESTful API's & applications with Symfony. Features include:

  • A View layer to enable output and format agnostic Controllers
  • Accept header format negotiation including handling for custom mime types
  • RESTful decoding of HTTP request body and Accept headers
  • Map exception codes to HTTP response status codes
  • A serializer error renderer that returns exceptions and errors in a format compatible with RFC 7807 using the Symfony Serializer component or the JMS Serializer  

Documentation

Read the Documentation

Please see the upgrade files (UPGRADING-X.X.md) for any relevant instructions when upgrading to a newer version.

Installation

All the installation instructions are located in the documentation.

Author: FriendsOfSymfony
Source Code: https://github.com/FriendsOfSymfony/FOSRestBundle 
License: MIT License

#php #symfony 

Provides Various tools To Rapidly Develop RESTful API's with Symfony

Generates Documentation for Your REST API From annotations

NelmioApiDocBundle

The NelmioApiDocBundle bundle allows you to generate a decent documentation for your APIs.

Migrate from 3.x to 4.0

Version 4.0 brings OpenAPI 3.0 support. If you want to stick to Swagger 2.0, you should use the version 3 of this bundle.

Upgrading From 3.x To 4.0

Version 4 is a major change introducing OpenAPI 3.0 support, the rebranded swagger specification, which brings a set of new interesting features. Unfortunately this required a rework to a large part of the bundle, and introduces BC breaks.

The Visual guide to "What's new in 3.0 spec" gives more information on OpenApi 3.0.

Version 4 does not support older versions of the specification. If you need to output swagger v2 documentation, you will need to use the latest 3.x release.

The Upgrade to Swagger 3.0

The biggest part of the upgrade will most likely be the upgrade of the library zircote/swagger-php to 3.0 which introduces new annotations in order to support OpenAPI 3.0 but also changes their namespace from Swagger to OpenApi.

They created a dedicated page to help you upgrade : https://github.com/zircote/swagger-php/blob/3.x/docs/Migrating-to-v3.md.

Here are some additional advices that are more likely to apply to NelmioApiDocBundle users:

Upgrade all your use Swagger\Annotations as SWG statements to use OpenApi\Annotations as OA; (to simplify the upgrade you may also stick to the SWG aliasing). In case you changed SWG to OA, upgrade all your annotations from @SWG\... to @OA\....

Update your config in case you used inlined swagger documentation (the field nelmio_api_doc.documentation). A tool is available to help you convert it.

In case you used @OA\Response(..., @OA\Schema(...)), you should explicit your media type by using the annotation @OA\JsonContent or @OA\XmlContent instead of @OA\Schema: @OA\Response(..., @OA\JsonContent(...)) or @OA\Response(..., @OA\XmlContent(...)).

When you use @Model directly (@OA\Response(..., @Model(...)))), the media type is set by default to json.

BC Breaks

There are also BC breaks that were introduced in 4.0:

We migrated from EXSyst\Component\Swagger\Swagger to OpenApi\Annotations\OpenApi to describe the api in all our describers signature (DescriberInterface, RouteDescriberInterface, ModelDescriberInterface, PropertyDescriberInterface).

PropertyDescriberInterface now takes several types as input to leverage compound types support in OpenApi 3.0 (int|string for instance).

Migrate from 2.x to 3.0

Upgrading From 2.x To 3.0

In 3.0 we did major changes. The biggest is the removal of the @ApiDoc annotation. To help you migrate to 3.0, we created this guide.

Thanks to a command created by @dbu, it should not take too long.

Step 1: Migrate to Swagger-PHP commands

First, copy this command in src/AppBundle/Command/SwaggerDocblockConvertCommand.php:

<?php

// src/AppBundle/Command/SwaggerDocblockConvertCommand.php
namespace AppBundle\Command;

use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Converts ApiDoc annotations to Swagger-PHP annotations.
 *
 * @author David Buchmann <david@liip.ch>
 * @author Guilhem Niot <guilhem.niot@gmail.com>
 */
class SwaggerDocblockConvertCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setDescription('')
            ->setName('api:doc:convert')
            ->addOption('views', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of views to convert the documentation for', 'default')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $views = explode(',', $input->getOption('views'));

        if (!$this->getContainer()->has('nelmio_api_doc.extractor.api_doc_extractor')) {
            if (!$this->getContainer()->has('nelmio_api_doc.controller.swagger_ui')) {
                throw new \RuntimeException('NelmioApiDocBundle is not installed. Please run `composer require nelmio/api-doc-bundle`.');
            } else {
                throw new \RuntimeException('This command only works with NelmioApiDocBundle 2.x installed while version 3.x is currently installed. Please downgrade to 2.x to execute this command and bump your constraint only after its execution.');                      
            }
        }

        $extractor = $this->getContainer()->get('nelmio_api_doc.extractor.api_doc_extractor');

        $apiDocs = [];
        foreach ($views as $view) {
            $apiDocs = array_merge($apiDocs, $extractor->extractAnnotations($extractor->getRoutes(), $view));
        }

        foreach ($apiDocs as $annotation) {
            /** @var ApiDoc $apiDoc */
            $apiDoc = $annotation['annotation'];

            $refl = $extractor->getReflectionMethod($apiDoc->getRoute()->getDefault('_controller'));

            $this->rewriteClass($refl->getFileName(), $refl, $apiDoc);
        }
    }

    /**
     * Rewrite class with correct apidoc.
     */
    private function rewriteClass(string $path, \ReflectionMethod $method, ApiDoc $apiDoc)
    {
        echo "Processing $path::{$method->name}\n";
        $code = file_get_contents($path);
        $old = $this->locateNelmioAnnotation($code, $method->name);

        $code = substr_replace($code, $this->renderSwaggerAnnotation($apiDoc, $method), $old['start'], $old['length']);
        $code = str_replace('use Nelmio\ApiDocBundle\Annotation\ApiDoc;', "use Nelmio\ApiDocBundle\Annotation\Operation;\nuse Nelmio\ApiDocBundle\Annotation\Model;\nuse Swagger\Annotations as SWG;", $code);

        file_put_contents($path, $code);
    }

    private function renderSwaggerAnnotation(ApiDoc $apiDoc, \ReflectionMethod $method): string
    {
        $info = $apiDoc->toArray();
        if ($apiDoc->getResource()) {
            throw new \RuntimeException('implement me');
        }
        $path = str_replace('.{_format}', '', $apiDoc->getRoute()->getPath());

        $annotation = '@Operation(
     *     tags={"'.$apiDoc->getSection().'"},
     *     summary="'.$this->escapeQuotes($apiDoc->getDescription()).'"';

        foreach ($apiDoc->getFilters() as $name => $parameter) {
            $description = array_key_exists('description', $parameter) && null !== $parameter['description']
                ? $this->escapeQuotes($parameter['description'])
                : 'todo';

            $annotation .= ',
     *     @SWG\Parameter(
     *         name="'.$name.'",
     *         in="query",
     *         description="'.$description.'",
     *         required='.(array_key_exists($name, $apiDoc->getRequirements()) ? 'true' : 'false').',
     *         type="'.$this->determineDataType($parameter).'"
     *     )';
        }

        // Put parameters for POST requests into formData, as Swagger cannot handle more than one body parameter
        $in = 'POST' === $apiDoc->getMethod()
            ? 'formData'
            : 'body';

        foreach ($apiDoc->getParameters() as $name => $parameter) {
            $description = array_key_exists('description', $parameter)
                ? $this->escapeQuotes($parameter['description'])
                : 'todo';

            $annotation .= ',
     *     @SWG\Parameter(
     *         name="'.$name.'",
     *         in="'.$in.'",
     *         description="'.$description.'",
     *         required='.(array_key_exists($name, $apiDoc->getRequirements()) ? 'true' : 'false');

            if ('POST' !== $apiDoc->getMethod()) {
                $annotation .= ',
     *         @SWG\Schema(type="'.$this->determineDataType($parameter).'")';
            } else {
                $annotation .= ',
     *         type="'.$this->determineDataType($parameter).'"';
            }

            $annotation .= '
     *     )';
        }

        if (array_key_exists('statusCodes', $info)) {
            $responses = $info['statusCodes'];
            foreach ($responses as $code => $description) {
                $responses[$code] = reset($description);
            }
        } else {
            $responses = [200 => 'Returned when successful'];
        }

        $responseMap = $apiDoc->getResponseMap();
        foreach ($responses as $code => $description) {
            $annotation .= ",
     *     @SWG\\Response(
     *         response=\"$code\",
     *         description=\"{$this->escapeQuotes($description)}\"";
            if (200 === $code && isset($responseMap[$code]['class'])) {
                $model = $responseMap[$code]['class'];
                $annotation .= ",
     *         @SWG\\Schema(ref=@Model(type=\"$model\"))";
            }
            $annotation .= '
     *     )';
        }

        $annotation .= '
     * )
     *';

        return $annotation;
    }

    /**
     * @return array with `start` position and `length`
     */
    private function locateNelmioAnnotation(string $code, string $methodName): array
    {
        $position = strpos($code, "tion $methodName(");
        if (false === $position) {
            throw new \RuntimeException("Method $methodName not found in controller.");
        }

        $docstart = strrpos(substr($code, 0, $position), '@ApiDoc');
        if (false === $docstart) {
            //If action is defined more than once. Should continue and don't throw exception
            $docstart = strrpos(substr($code, 0, $position), '@Operation');
            if (false === $docstart) {

                throw new \RuntimeException("Method $methodName has no @ApiDoc annotation around\n".substr($code, $position - 200, 150));
            }
        }
        $docend = strpos($code, '* )', $docstart) + 3;

        return [
            'start' => $docstart,
            'length' => $docend - $docstart,
        ];
    }

    private function escapeQuotes(string $str = null): string
    {
        if ($str === null) {
            return '';
        }
        $lines = [];
        foreach (explode("\n", $str) as $line) {
            $lines[] = trim($line, ' *');
        }

        return str_replace('"', '""', implode(' ', $lines));
    }

    private function determineDataType(array $parameter): string
    {
        $dataType = isset($parameter['dataType']) ? $parameter['dataType'] : 'string';
        $transform = [
            'float' => 'number',
            'datetime' => 'string',
        ];
        if (array_key_exists($dataType, $transform)) {
            $dataType = $transform[$dataType];
        }

        return $dataType;
    }
}

Then open a command console, enter your project directory and run:

bin/console api:doc:convert

Your annotations should all be converted.

Note that this tool is here to help you but not all features are supported so make sure the generated annotations still fit your needs.

Step 2: Update your routing

With NelmioApiDocBundle 2.x, you had to load the @NelmioApiDocBundle/Resources/config/routing.yml file. In 3.0, it was renamed to @NelmioApiDocBundle/Resources/config/routing/swaggerui.xml, so you have to update the NelmioApiDocBundle route to:

# app/config/routing.yml
NelmioApiDocBundle:
    resource: "@NelmioApiDocBundle/Resources/config/routing/swaggerui.xml"
    prefix:   /api/doc

Step 3: Update your config

nelmio_api_doc.name was replaced by nelmio_api_doc.documentation.info.title.

Before:

nelmio_api_doc:
    name: My Awesome App!

After:

nelmio_api_doc:
    documentation:
        info:
            title: My Awesome App!

nelmio_api_doc.swagger.api_version was replaced by nelmio_api_doc.documentation.info.version.

nelmio_api_doc.swagger.info.title was replaced by nelmio_api_doc.documentation.info.title.

nelmio_api_doc.swagger.info.description was replaced by nelmio_api_doc.documentation.info.description.

Other options were removed.

Step 4: Update the bundle

Change the constraint of nelmio/api-doc-bundle in your composer.json file to ^3.0:

{
    "require": {
        "nelmio/api-doc-bundle": "^3.0"
    }
}

Then update your dependencies:

composer update

Step 5: Review the changes

It's almost finished!

As most of the changes were automated you should check that they did not break anything. Run your test suite, review, do whatever you think is useful before pushing the changes.

Then, commit the changes, push them, and enjoy!

Installation

Open a command console, enter your project directory and execute the following command to download the latest version of this bundle:

composer require nelmio/api-doc-bundle

Documentation

Read the documentation on symfony.com

Contributing

See CONTRIBUTING file.

Running the Tests

Install the Composer dependencies:

git clone https://github.com/nelmio/NelmioApiDocBundle.git
cd NelmioApiDocBundle
composer update

Then run the test suite:

./phpunit

Author: Nelmio
Source Code: https://github.com/nelmio/NelmioApiDocBundle 
License: MIT License

#php #symfony #api 

Generates Documentation for Your REST API From annotations

FOSOAuthServerBundle: A Server Side OAuth2 Bundle for Symfony

FOSOAuthServerBundle

Getting Started With FOSOAuthServerBundle

Prerequisites

This version of the bundle requires Symfony 2.8. If you are using Symfony 2.0.x, please use the 1.1.1 release of the bundle (or lower), and follow this documentation.

Translations

If you wish to use default texts provided in this bundle, you have to make sure you have translator enabled in your config:

# app/config/config.yml

framework:
    translator: { fallback: en }

For more information about translations, check Symfony documentation.

Installation

Installation is a quick 5 steps process:

  1. Download FOSOAuthServerBundle
  2. Enable the Bundle
  3. Create your model class
  4. Configure your application's security.yml
  5. Configure the FOSOAuthServerBundle

Step 1: Install FOSOAuthServerBundle

The preferred way to install this bundle is to rely on Composer. Just check on Packagist the version you want to install (in the following example, we used "dev-master") and add it to your composer.json:

{
    "require": {
        // ...
        "friendsofsymfony/oauth-server-bundle": "dev-master"
    }
}

Step 2: Enable the bundle

Finally, enable the bundle in the kernel:

<?php
// app/AppKernel.php

public function registerBundles()
{
    $bundles = array(
        // ...
        new FOS\OAuthServerBundle\FOSOAuthServerBundle(),
    );
}

Step 3: Create model classes

This bundle needs to persist some classes to a database:

  • Client (OAuth2 consumers)
  • AccessToken
  • RefreshToken
  • AuthCode

Your first job, then, is to create these classes for your application. These classes can look and act however you want: add any properties or methods you find useful.

These classes have just a few requirements:

  1. They must extend one of the base classes from the bundle
  2. They must have an id field

In the following sections, you'll see examples of how your classes should look, depending on how you're storing your data.

Your classes can live inside any bundle in your application. For example, if you work at "Acme" company, then you might create a bundle called AcmeApiBundle and place your classes in it.

Warning:

If you override the __construct() method in your classes, be sure to call parent::__construct(), as the base class depends on this to initialize some fields.

Doctrine ORM classes

If you're persisting your data via the Doctrine ORM, then your classes should live in the Entity namespace of your bundle and look like this to start:

<?php
// src/Acme/ApiBundle/Entity/Client.php

namespace Acme\ApiBundle\Entity;

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

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

    public function __construct()
    {
        parent::__construct();
        // your own logic
    }
}
<?php
// src/Acme/ApiBundle/Entity/AccessToken.php

namespace Acme\ApiBundle\Entity;

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

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

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

    /**
     * @ORM\ManyToOne(targetEntity="Your\Own\Entity\User")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
     */
    protected $user;
}
<?php
// src/Acme/ApiBundle/Entity/RefreshToken.php

namespace Acme\ApiBundle\Entity;

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

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

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

    /**
     * @ORM\ManyToOne(targetEntity="Your\Own\Entity\User")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
     */
    protected $user;
}
<?php
// src/Acme/ApiBundle/Entity/AuthCode.php

namespace Acme\ApiBundle\Entity;

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

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

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

    /**
     * @ORM\ManyToOne(targetEntity="Your\Own\Entity\User")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
     */
    protected $user;
}

Note: If you don't have auto_mapping activated in your doctrine configuration you need to add FOSOAuthServerBundle to your mappings in config.yml.

Doctrine ODM classes

<?php

// src/Acme/ApiBundle/Document/Client.php

namespace Acme\ApiBundle\Document;

use FOS\OAuthServerBundle\Document\Client as BaseClient;

class Client extends BaseClient
{
    protected $id;
}
<!-- src/Acme/ApiBundle/Resources/config/doctrine/Client.mongodb.xml -->

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\ApiBundle\Document\Client" db="acme" collection="oauthClient" customId="true">
        <field fieldName="id" id="true" strategy="AUTO" />
    </document>

</doctrine-mongo-mapping>
<?php

// src/Acme/ApiBundle/Document/AuthCode.php

namespace Acme\ApiBundle\Document;

use FOS\OAuthServerBundle\Document\AuthCode as BaseAuthCode;
use FOS\OAuthServerBundle\Model\ClientInterface;

class AuthCode extends BaseAuthCode
{
    protected $id;
    protected $client;

    public function getClient()
    {
        return $this->client;
    }

    public function setClient(ClientInterface $client)
    {
        $this->client = $client;
    }
}
<!-- src/Acme/ApiBundle/Resources/config/doctrine/AuthCode.mongodb.xml -->

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\ApiBundle\Document\AuthCode" db="acme" collection="oauthAuthCode" customId="true">
        <field fieldName="id" id="true" strategy="AUTO" />
        <reference-one target-document="Acme\ApiBundle\Document\Client" field="client" />
    </document>

</doctrine-mongo-mapping>
<?php

// src/Acme/ApiBundle/Document/AccessToken.php

namespace Acme\ApiBundle\Document;

use FOS\OAuthServerBundle\Document\AccessToken as BaseAccessToken;
use FOS\OAuthServerBundle\Model\ClientInterface;

class AccessToken extends BaseAccessToken
{
    protected $id;
    protected $client;

    public function getClient()
    {
        return $this->client;
    }

    public function setClient(ClientInterface $client)
    {
        $this->client = $client;
    }
}
<!-- src/Acme/ApiBundle/Resources/config/doctrine/AccessToken.mongodb.xml -->

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\ApiBundle\Document\AccessToken" db="acme" collection="oauthAccessToken" customId="true">
        <field fieldName="id" id="true" strategy="AUTO" />
        <reference-one target-document="Acme\ApiBundle\Document\Client" field="client" />
    </document>

</doctrine-mongo-mapping>
<?php

// src/Acme/ApiBundle/Document/RefreshToken.php

namespace Acme\ApiBundle\Document;

use FOS\OAuthServerBundle\Document\RefreshToken as BaseRefreshToken;
use FOS\OAuthServerBundle\Model\ClientInterface;

class RefreshToken extends BaseRefreshToken
{
    protected $id;
    protected $client;

    public function getClient()
    {
        return $this->client;
    }

    public function setClient(ClientInterface $client)
    {
        $this->client = $client;
    }
}
<!-- src/Acme/ApiBundle/Resources/config/doctrine/RefreshToken.mongodb.xml -->

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\ApiBundle\Document\RefreshToken" db="acme" collection="oauthRefreshToken" customId="true">
        <field fieldName="id" id="true" strategy="AUTO" />
        <reference-one target-document="Acme\ApiBundle\Document\Client" field="client" />
    </document>

</doctrine-mongo-mapping>

Step 4: Configure your application's security.yml

In order for Symfony's security component to use the FOSOAuthServerBundle, you must tell it to do so in the security.yml file. The security.yml file is where the basic configuration for the security for your application is contained.

Below is a minimal example of the configuration necessary to use the FOSOAuthServerBundle in your application:

# app/config/security.yml
security:
    firewalls:
        oauth_token:
            pattern:    ^/oauth/v2/token
            security:   false

        oauth_authorize:
            pattern:    ^/oauth/v2/auth
            # Add your favorite authentication process here

        api:
            pattern:    ^/api
            fos_oauth:  true
            stateless:  true
            anonymous:  false # can be omitted as its default value

    access_control:
        - { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }

The URLs under /api will use OAuth2 to authenticate users.

Anonymous access

Sometimes you need to allow your api to be accessed without authorization. In order to do that lets adjust above-mentioned example configuration.

# app/config/security.yml
security:
    firewalls:
        oauth_token:
            pattern:    ^/oauth/v2/token
            security:   false

        oauth_authorize:
            pattern:    ^/oauth/v2/auth
            # Add your favorite authentication process here

        api:
            pattern:    ^/api
            fos_oauth:  true
            stateless:  true
            anonymous:  true # note that anonymous access is now enabled

    # also note absence of "access_control" section

From now on all of your api resources can be accessed without authorization. But what if one or more of them should be secured anyway or/and require presence of authenticated user? It's easy! You can do that manually by adding few lines of code at the beginning of all of your secured actions like in the example below:

// [...]
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class YourApiController extends Controller
{
    public function getSecureResourceAction()
    {
        # this is it
        if (false === $this->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY')) {
            throw new AccessDeniedException();
        }

        // [...]
    }

Step 5: Configure FOSOAuthServerBundle

Import the routing.yml configuration file in app/config/routing.yml:

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

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

Add FOSOAuthServerBundle settings in app/config/config.yml:

# app/config/config.yml
fos_oauth_server:
    db_driver: orm       # Drivers available: orm or mongodb
    client_class:        Acme\ApiBundle\Entity\Client
    access_token_class:  Acme\ApiBundle\Entity\AccessToken
    refresh_token_class: Acme\ApiBundle\Entity\RefreshToken
    auth_code_class:     Acme\ApiBundle\Entity\AuthCode

If you're authenticating users, don't forget to set the user provider. Here's an example using the FOSUserBundle user provider:

# app/config/config.yml
fos_oauth_server:
    ...

    service:
        user_provider: fos_user.user_provider.username

Creating A Client

Console Command

The most convenient way to create a client is to use the console command.

$ php app/console fos:oauth-server:create-client --redirect-uri="..." --grant-type="..."

Note: you can use --redirect-uri and --grant-type multiple times to add additional values.

Programatically

Before you can generate tokens, you need to create a Client using the ClientManager.

$clientManager = $this->container->get('fos_oauth_server.client_manager.default');
$client = $clientManager->createClient();
$client->setRedirectUris(array('http://www.example.com'));
$client->setAllowedGrantTypes(array('token', 'authorization_code'));
$clientManager->updateClient($client);

Once you have created a client, you need to pass its publicId to the authorize endpoint. You also need to specify a redirect uri as well as a response type.

return $this->redirect($this->generateUrl('fos_oauth_server_authorize', array(
    'client_id'     => $client->getPublicId(),
    'redirect_uri'  => 'http://www.example.com',
    'response_type' => 'code'
)));

Usage

The token endpoint is at /oauth/v2/token by default (see Resources/config/routing/token.xml).

The authorize endpoint is at /oauth/v2/auth by default (see Resources/config/routing/authorize.xml).

Next steps

A Note About Security

Configuration Reference

Dealing With Scopes

Extending the Authorization page

Extending the Model

The OAuthEvent class

Adding Grant Extensions

Custom DB Driver

Documentation

The bulk of the documentation is stored in the Resources/doc/index.md file in this bundle:

Read the Documentation for master

TODO

  • More tests

Credits

Author: FriendsOfSymfony
Source Code: https://github.com/FriendsOfSymfony/FOSOAuthServerBundle 
License: MIT license

#php #symfony 

FOSOAuthServerBundle: A Server Side OAuth2 Bundle for Symfony

Automatic CSRF Protection for JavaScript Apps using A Symfony API

JavaScript CSRF Protection Bundle

Archived! Now that all modern browsers implement SameSite cookies and the Origin HTTP header, this bundle is - in most cases - not necessary anymore. Learn how to protect your Symfony APIs from CSRF attacks. If you need to maintain old applications, take a look to DneustadtCsrfCookieBundle.

This API Platform and Symfony bundle provides automatic Cross Site Request Forgery (CSRF or XSRF) protection for client-side applications.

Despite the name, it works with any client-side technology including Angular, React, Vue.js and jQuery. Actually, any JavaScript code issuing XMLHttpRequest or using the Fetch API can leverage this bundle.

How it Works

Thanks to this bundle, the server-side application (the Symfony app) will automatically set a cookie named XSRF-Token containing a unique token during the first HTTP response sent to the browser. Subsequent asynchronous requests made by the JavaScript app with xhr or fetch send back the value of the cookie in a special HTTP header named X-XSRF-Token.

To prevent CSRF attacks, the bundle will check that the header's value match the cookie's value. This way, it will be able to detect and block CSRF attacks.

AngularJS (v1)'s ng.$http service has a built-in support for this CSRF protection system. If you use another framework or HTTP client (such as Axios), you just need to read the cookie value and add the HTTP header containing it by yourself.

This bundle provides a Symfony's Event Listener that set the cookie and another one that checks the HTTP header to block CSRF attacks.

Thanks to DunglasAngularCsrfBundle, you get CSRF security without modifying your code base.

This bundle works fine with both API Platform and FOSRestBundle.

Installation

Use Composer to install this bundle:

composer require dunglas/angular-csrf-bundle

If you use Symfony Flex, you're done.

Otherwise add the bundle in your application kernel:

// app/AppKernel.php

public function registerBundles()
{
    return array(
        // ...
        new Dunglas\AngularCsrfBundle\DunglasAngularCsrfBundle(),
        // ...
    );
}

Configure URLs where the cookie must be set and that must be protected against CSRF attacks:

# app/config/security.yml
dunglas_angular_csrf:
    # Collection of patterns where to set the cookie
    cookie:
        set_on:
            - { path: ^/$ }
            - { route: ^app_, methods: [GET, HEAD] }
            - { host: example.com }
    # Collection of patterns to secure
    secure:
        - { path: ^/api, methods: [POST, PUT, PATCH, LINK] }
        - { route: ^api_v2_ }
        - { host: example.com, methods: [POST, PUT, PATCH, DELETE, LINK] }
    # Collection of patterns to exclude
    exclude:
        - { path: ^/api/exclude, methods: [POST, PUT, PATCH, LINK] }
        - { route: ^api_v2_exclude }
        - { host: exclude-example.com, methods: [POST, PUT, PATCH, DELETE, LINK] }
        

Your app is now secured.

Examples

  • DunglasTodoMVCBundle: an implementation of the TodoMVC app using Symfony, Backbone.js and Chaplin.js

Full Configuration

dunglas_angular_csrf:
    token:
        # The CSRF token id
        id: angular
    header:
        # The name of the HTTP header to check (default to the AngularJS default)
        name: X-XSRF-TOKEN
    cookie:
        # The name of the cookie to set (default to the AngularJS default)
        name: XSRF-TOKEN
        # Expiration time of the cookie
        expire: 0
        # Path of the cookie
        path: /
        # Domain of the cookie
        domain: ~
        # If true, set the cookie only on HTTPS connection
        secure: false
        # Patterns of URLs to set the cookie
        set_on:
            - { path: "^/url-pattern", route: "^route_name_pattern$", host: "example.com", methods: [GET, POST] }
    # Patterns of URLs to check for a valid CSRF token
    secure:
        - { path: "^/url-pattern", route: "^route_name_pattern$", host: "example.com", methods: [GET, POST] }
    # Patterns to exclude from secure routes
    exclude:
        - { path: "^/url-pattern/exclude", route: "^route_name_pattern$", host: "example.com", methods: [GET, POST] }

Integration with the Symfony Form Component

When using the Symfony Form Component together with DunglasAngularCsrfBundle, the bundle will automatically disable the built-in form CSRF protection only if the CSRF token provided by the header is valid.

If no CSRF header is found or if the token is invalid, the form CSRF protection will not be disabled by the bundle.

If you want your form to be validated only by the form component system, make sure to remove its URL from the config.

Credits

This bundle has been created by Kévin Dunglas.

Author: Dunglas
Source Code: https://github.com/dunglas/DunglasAngularCsrfBundle 
License: MIT License

#php #symfony #javascript 

Automatic CSRF Protection for JavaScript Apps using A Symfony API

BazingaHateoasBundle: integration Of The Hateoas Library Into Symfony

BazingaHateoasBundle   

This bundle integrates Hateoas with Symfony.

Installation

Step 1: Download the Bundle

Open a command console, enter your project directory and execute the following command to download the latest stable version of this bundle:

$ composer require willdurand/hateoas-bundle

This command requires you to have Composer installed globally, as explained in the installation chapter of the Composer documentation.

Step 2: Enable the Bundle

Note: this step is not required if you are using Symfony Flex

Then, enable the bundle by adding the following line in the app/AppKernel.php file of your project:

<?php
// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...
            new Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle(),
        );
        
        // ...
    }
}

Note: The bundle requires the JMSSerializerBundle to be registered. If you haven't done that already, you should register it in the kernel aswell:

// app/AppKernel.php

// ...
public function registerBundles()
{
    $bundles = array(
        // ...
        new JMS\SerializerBundle\JMSSerializerBundle(),
    );

    // ...
}

Usage

Mapping Objects

Refer to the Hateoas documentation to find out how to map your objects.

Serializing objects

The BazingaHateoasBundle transparently hooks into the JMS serializer, there are no special considerations:

// My/Controller.php

class SomeController extends Controller
{
    public function resourceAction(Request $request)
    {
        $post = $repository->find('BlogBundle:post');
        $json = $this->container->get('jms_serializer')->serialize($post, 'json');

        return new Response($json, 200, array('Content-Type' => 'application/json'));
    }
}

An example using dependency injection:

// My/Controller.php

use JMS\Serializer\SerializerInterface; 

class SomeController extends Controller
{
    private $serializer;
    
    public function __consttruct(SerializerInterface $serializer)
    {
        $this->serializer = $serializer;
    }
    
    public function resourceAction(Request $request)
    {
        $post = $repository->find('BlogBundle:post');
        $json = $this->serializer->serialize($post, 'json');

        return new Response($json, 200, array('Content-Type' => 'application/json'));
    }
}

Expression Language

This bundle provides three extra functions to the expression language:

is_granted

Allows you to exclude certain routes by checking whether the currently authenticated user has certain permissions or not. For example:

use Hateoas\Configuration\Annotation as Hateoas;

/**
 * @Hateoas\Relation(
 *      "delete",
 *      href = @Hateoas\Route(
 *          "post_delete",
 *          parameters = { "id" = "expr(object.getId())" }
 *      ),
 *      exclusion = @Hateoas\Exclusion(
 *          excludeIf = "expr(not is_granted('ROLE_ADMIN'))"
 *      )
 * )
 */
class Post
{
    // ...
}

If the authenticated user has the ROLE_ADMIN role the route will be exposed, otherwise the route will be excluded.

parameter

Allows you to fetch a parameter from the service container:

/**
 * @Hateoas\Relation(
 *      "delete",
 *      href = @Hateoas\Route(
 *          "post_delete",
 *          parameters = { "foo" = "expr(parameter('foo'))" }
 *      )
 * )
 */
class Post
{
    // ...
}

service

Allows you to fetch a service from the service container.

Extending

RelationProvider

A relation provider provide relations (links) for a given class.

You can add new relation providers by implementing the Hateoas\Configuration\Provider\RelationProviderInterface interface and adding a definition to the dependency injection configuration with the tag hateoas.relation_provider:

<?xml version="1.0" ?>
<container ...>

    <!-- ... -->

    <services>
        <!-- ... -->

        <service id="acme_foo.hateoas.relation_provider.foobar" class="Acme\FooBundle\Hateoas\RelationProvider\Foobar">
            <tag name="hateoas.relation_provider" />
        </service>
    </services>
</container>

Configuration Extension

A configuration extension allows you to override already configured relations.

You can add an extension configuration by adding a definition to the dependency injection configuration with the tag hateoas.configuration_extension:

<?xml version="1.0" ?>
<container ...>

    <!-- ... -->

    <services>
        <!-- ... -->

        <service id="acme_foo.hateoas.configuration_extension.foobar" class="Acme\FooBundle\Hateoas\ConfigurationExtension\AcmeFooConfigurationExtension">
            <tag name="hateoas.configuration_extension" />
        </service>
    </services>
</container>
<?php
namespace Acme\FooBundle\Hateoas\ConfigurationExtension;

use Hateoas\Configuration\Metadata\ConfigurationExtensionInterface;
use Hateoas\Configuration\Metadata\ClassMetadataInterface;
use Hateoas\Configuration\Relation;

class AcmeFooConfigurationExtension implements ConfigurationExtensionInterface
{
    /**
     * {@inheritDoc}
     */
    public function decorate(ClassMetadataInterface $classMetadata)
    {
        if (0 === strpos('Acme\Foo\Model', $classMetadata->getName())) {
            // Add a "root" relation to all classes in the `Acme\Foo\Model` namespace
            $classMetadata->addRelation(
                new Relation(
                    'root',
                    '/'
                )
            );
        }
    }
}

Reference Configuration

# app/config/config*.yml

bazinga_hateoas:
    metadata:
        cache:                file
        file_cache:
            dir:              '%kernel.cache_dir%/hateoas'
    serializer:
        json: hateoas.serializer.json_hal
        xml: hateoas.serializer.xml
    twig_extension:
        enabled: true

Testing

Setup the test suite using Composer:

$ composer install --dev

Run it using PHPUnit:

$ phpunit

Documentation

For documentation, see:

Resources/doc/

Read the documentation

Contributing

See CONTRIBUTING file.

Credits

Author: Willdurand
Source Code: https://github.com/willdurand/BazingaHateoasBundle 
License: MIT License

#php #symfony 

BazingaHateoasBundle: integration Of The Hateoas Library Into Symfony