Simple API V1 implemented using Symfony Serializer.

This commit is contained in:
Alexey Skobkin 2018-06-22 22:45:17 +03:00
parent d1eefb3d12
commit 027476c553
13 changed files with 618 additions and 23 deletions

View File

@ -21,6 +21,7 @@
"symfony/framework-bundle": "^4.1",
"symfony/lts": "^4@dev",
"symfony/orm-pack": "^1.0",
"symfony/serializer-pack": "^1.0",
"symfony/translation": "^4.1",
"symfony/twig-bundle": "^4.1",
"symfony/web-server-bundle": "^4.1",

391
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6451fb298116549e08f8a9860dc00786",
"content-hash": "fcc11531a19d6444ffb2125e69ff29bd",
"packages": [
{
"name": "doctrine/annotations",
@ -1166,6 +1166,158 @@
],
"time": "2018-05-17T09:23:52+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
"reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.6"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": [
"src"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jaap van Otterdijk",
"email": "opensource@ijaap.nl"
}
],
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
"homepage": "http://www.phpdoc.org",
"keywords": [
"FQSEN",
"phpDocumentor",
"phpdoc",
"reflection",
"static analysis"
],
"time": "2017-09-11T18:02:19+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "4.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "94fd0001232e47129dd3504189fa1c7225010d08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08",
"reference": "94fd0001232e47129dd3504189fa1c7225010d08",
"shasum": ""
},
"require": {
"php": "^7.0",
"phpdocumentor/reflection-common": "^1.0.0",
"phpdocumentor/type-resolver": "^0.4.0",
"webmozart/assert": "^1.0"
},
"require-dev": {
"doctrine/instantiator": "~1.0.5",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^6.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"time": "2017-11-30T07:14:17+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "0.4.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
"reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
"shasum": ""
},
"require": {
"php": "^5.5 || ^7.0",
"phpdocumentor/reflection-common": "^1.0"
},
"require-dev": {
"mockery/mockery": "^0.9.4",
"phpunit/phpunit": "^5.2||^4.8.24"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
}
],
"time": "2017-07-14T14:27:02+00:00"
},
{
"name": "psr/cache",
"version": "1.0.1",
@ -2650,6 +2802,82 @@
],
"time": "2018-05-30T07:26:09+00:00"
},
{
"name": "symfony/property-info",
"version": "v4.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-info.git",
"reference": "724cca5ae45760156029f14d2e293a281fab89e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/property-info/zipball/724cca5ae45760156029f14d2e293a281fab89e0",
"reference": "724cca5ae45760156029f14d2e293a281fab89e0",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"symfony/inflector": "~3.4|~4.0"
},
"conflict": {
"phpdocumentor/reflection-docblock": "<3.0||>=3.2.0,<3.2.2",
"phpdocumentor/type-resolver": "<0.2.1",
"symfony/dependency-injection": "<3.4"
},
"require-dev": {
"doctrine/annotations": "~1.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0",
"symfony/cache": "~3.4|~4.0",
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/serializer": "~3.4|~4.0"
},
"suggest": {
"phpdocumentor/reflection-docblock": "To use the PHPDoc",
"psr/cache-implementation": "To cache results",
"symfony/doctrine-bridge": "To use Doctrine metadata",
"symfony/serializer": "To use Serializer metadata"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.1-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\PropertyInfo\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Property Info Component",
"homepage": "https://symfony.com",
"keywords": [
"doctrine",
"phpdoc",
"property",
"symfony",
"type",
"validator"
],
"time": "2018-05-16T14:33:22+00:00"
},
{
"name": "symfony/routing",
"version": "v4.1.0",
@ -2728,6 +2956,117 @@
],
"time": "2018-05-30T07:26:09+00:00"
},
{
"name": "symfony/serializer",
"version": "v4.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer.git",
"reference": "db427d70438645789ffce6048d61b3992118a33a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/serializer/zipball/db427d70438645789ffce6048d61b3992118a33a",
"reference": "db427d70438645789ffce6048d61b3992118a33a",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"phpdocumentor/type-resolver": "<0.2.1",
"symfony/dependency-injection": "<3.4",
"symfony/property-access": "<3.4",
"symfony/property-info": "<3.4",
"symfony/yaml": "<3.4"
},
"require-dev": {
"doctrine/annotations": "~1.0",
"doctrine/cache": "~1.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0",
"symfony/cache": "~3.4|~4.0",
"symfony/config": "~3.4|~4.0",
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/http-foundation": "~3.4|~4.0",
"symfony/property-access": "~3.4|~4.0",
"symfony/property-info": "~3.4|~4.0",
"symfony/validator": "~3.4|~4.0",
"symfony/yaml": "~3.4|~4.0"
},
"suggest": {
"doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.",
"doctrine/cache": "For using the default cached annotation reader and metadata cache.",
"psr/cache-implementation": "For using the metadata cache.",
"symfony/config": "For using the XML mapping loader.",
"symfony/http-foundation": "To use the DataUriNormalizer.",
"symfony/property-access": "For using the ObjectNormalizer.",
"symfony/property-info": "To deserialize relations.",
"symfony/yaml": "For using the default YAML mapping loader."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.1-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Serializer\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Serializer Component",
"homepage": "https://symfony.com",
"time": "2018-05-30T07:26:09+00:00"
},
{
"name": "symfony/serializer-pack",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer-pack.git",
"reference": "35cea385ea44d1f40ec12571996bf768fbe409de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/serializer-pack/zipball/35cea385ea44d1f40ec12571996bf768fbe409de",
"reference": "35cea385ea44d1f40ec12571996bf768fbe409de",
"shasum": ""
},
"require": {
"doctrine/annotations": "^1.0",
"php": "^7.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0",
"symfony/cache": "^3.3|^4.0",
"symfony/property-access": "^3.3|^4.0",
"symfony/property-info": "^3.3|^4.0",
"symfony/serializer": "^3.3|^4.0"
},
"type": "symfony-pack",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A pack for the Symfony serializer",
"time": "2017-12-12T01:48:53+00:00"
},
{
"name": "symfony/translation",
"version": "v4.1.0",
@ -3145,6 +3484,56 @@
],
"time": "2018-04-02T09:24:19+00:00"
},
{
"name": "webmozart/assert",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.6",
"sebastian/version": "^1.0.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"time": "2018-01-29T19:49:41+00:00"
},
{
"name": "white-october/pagerfanta-bundle",
"version": "v1.2.1",

View File

@ -1,16 +1,37 @@
# Basic Front-End
index:
path: /
controller: App\Controller\MainController::index
torrent_show:
torrents_search:
path: /torrents/search
controller: App\Controller\TorrentController::searchTorrent
requirements:
method: GET
torrents_show:
path: /torrents/{id}
controller: App\Controller\TorrentController::showTorrent
requirements:
method: GET
id: '\d+'
torrent_search:
path: /torrents/search
controller: App\Controller\TorrentController::searchTorrent
# API
api_v1_torrents_search:
path: /api/v1/torrents/search
controller: App\Api\V1\Controller\TorrentController::search
defaults:
_format: json
requirements:
method: GET
method: GET
_format: json
api_v1_torrents_show:
path: /api/v1/torrents/{id}
controller: App\Api\V1\Controller\TorrentController::show
defaults:
_format: json
requirements:
method: GET
_format: json
id: '\d+'

View File

@ -22,3 +22,10 @@ services:
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
# Fast normalizer for Symfony Serializer
get_set_method_normalizer:
class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
public: false
tags:
- { name: serializer.normalizer, priority: 1 }

View File

@ -0,0 +1,46 @@
<?php
namespace App\Api\V1\Controller;
use App\Api\V1\DTO\ApiResponse;
use App\Entity\Torrent;
use App\Repository\TorrentRepository;
use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Pagerfanta;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\{Request, Response};
class TorrentController extends Controller
{
private const DEFAULT_SERIALIZER_GROUPS = ['api_v1'];
private const PER_PAGE = 20;
public function search(Request $request, TorrentRepository $repo): Response
{
$query = $request->query->get('query', '');
$page = (int) $request->query->get('page', '1');
$pagerAdapter = new DoctrineORMAdapter($repo->createFindLikeQueryBuilder($query));
$pager = new Pagerfanta($pagerAdapter);
$pager
->setCurrentPage($page)
->setMaxPerPage(self::PER_PAGE)
;
return $this->json(new ApiResponse($pager->getCurrentPageResults()),Response::HTTP_OK, [], [
'groups' => array_merge(self::DEFAULT_SERIALIZER_GROUPS,['api_v1_search']),
]);
}
public function show(Torrent $torrent): Response
{
return $this->json(new ApiResponse($torrent), Response::HTTP_OK, [], [
'groups' => array_merge(self::DEFAULT_SERIALIZER_GROUPS,['api_v1_show']),
]);
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace App\Api\V1\DTO;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Annotation\{Groups, MaxDepth};
class ApiResponse
{
public const STATUS_SUCCESS = 'success';
public const STATUS_ERROR = 'error';
public const STATUS_FAIL = 'fail';
public const STATUS_UNKNOWN = 'unknown';
/**
* @var int HTTP response status code
*
* @Groups({"api_v1"})
*/
private $code;
/**
* @var string Status text: 'success' (1xx-3xx), 'error' (4xx), 'fail' (5xx) or 'unknown'
*
* @Groups({"api_v1"})
*/
private $status;
/**
* @var string|null Used for 'fail' and 'error'
*
* @Groups({"api_v1"})
*/
private $message;
/**
* @var string|\object|array|null Response body. In case of 'error' or 'fail' contains cause or exception name.
*
* @Groups({"api_v1"})
*/
private $data;
public function __construct($data = null, int $code = Response::HTTP_OK, string $message = null, string $status = '')
{
$this->data = $data;
$this->code = $code;
$this->message = $message;
if ('' === $status) {
switch ($code) {
case ($code >= 100 && $code < 300):
$this->status = self::STATUS_SUCCESS;
break;
case ($code >= 400 && $code < 500):
$this->status = self::STATUS_ERROR;
break;
case ($code >= 500 && $code < 600):
$this->status = self::STATUS_FAIL;
break;
default:
$this->status = self::STATUS_UNKNOWN;
}
} else {
$this->status = $status;
}
}
public function getCode(): int
{
return $this->code;
}
public function getStatus(): string
{
return $this->status;
}
public function getMessage(): ?string
{
return $this->message;
}
/** @return array|\object|string|null */
public function getData()
{
return $this->data;
}
}

View File

@ -4,10 +4,11 @@ namespace App\Controller;
use App\Repository\TorrentRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class MainController extends Controller
{
public function index(TorrentRepository $repo)
public function index(TorrentRepository $repo): Response
{
return $this->render('index.html.twig', [
'torrentsCount' => $repo->getTorrentsTotalCount(),

View File

@ -8,19 +8,13 @@ use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Pagerfanta;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class TorrentController extends Controller
{
private const PER_PAGE = 20;
public function showTorrent(Torrent $torrent)
{
return $this->render('torrent_show.html.twig', [
'torrent' => $torrent,
]);
}
public function searchTorrent(Request $request, TorrentRepository $repo)
public function searchTorrent(Request $request, TorrentRepository $repo): Response
{
$query = $request->query->get('query', '');
$page = (int) $request->query->get('page', '1');
@ -37,4 +31,11 @@ class TorrentController extends Controller
'searchQuery' => $query,
]);
}
public function showTorrent(Torrent $torrent): Response
{
return $this->render('torrent_show.html.twig', [
'torrent' => $torrent,
]);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Table(name="files", indexes={
@ -31,6 +32,8 @@ class File
/**
* @var int File size in bytes
*
* @Serializer\Groups({"api_v1_show"})
*
* @ORM\Column(name="size", type="integer", nullable=false)
*/
private $size;
@ -38,6 +41,8 @@ class File
/**
* @var string
*
* @Serializer\Groups({"api_v1_show"})
*
* @ORM\Column(name="path", type="text", nullable=false)
*/
private $path;

View File

@ -4,6 +4,7 @@ namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Table(name="torrents", indexes={
@ -17,6 +18,8 @@ class Torrent
/**
* @var int
*
* @Serializer\Groups({"api_v1_search", "api_v1_show"})
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
*/
@ -25,6 +28,8 @@ class Torrent
/**
* @var resource Resource pointing to info-hash BLOB
*
* @Serializer\Groups({"api_v1_search", "api_v1_show"})
*
* @ORM\Column(name="info_hash", type="blob", nullable=false)
*/
private $infoHash;
@ -37,6 +42,8 @@ class Torrent
/**
* @var string Torrent name
*
* @Serializer\Groups({"api_v1_search", "api_v1_show"})
*
* @ORM\Column(name="name", type="text", nullable=false)
*/
private $name;
@ -44,6 +51,8 @@ class Torrent
/**
* @var int Torrent files total size in bytes
*
* @Serializer\Groups({"api_v1_search", "api_v1_show"})
*
* @ORM\Column(name="total_size", type="integer", nullable=false)
*/
private $totalSize;
@ -51,6 +60,8 @@ class Torrent
/**
* @var int Torrent discovery timestamp
*
* @Serializer\Groups({"api_v1_search", "api_v1_show"})
*
* @ORM\Column(name="discovered_on", type="integer", nullable=false)
*/
private $discoveredOn;
@ -58,6 +69,8 @@ class Torrent
/**
* @var File[]|ArrayCollection
*
* @Serializer\Groups({"api_v1_show"})
*
* @ORM\OneToMany(targetEntity="App\Entity\File", fetch="EXTRA_LAZY", mappedBy="torrent")
*/
private $files;
@ -68,16 +81,18 @@ class Torrent
}
/**
* Returns torrent info hash BLOB resource
*
* @return resource
* Returns torrent info hash as HEX string
*/
public function getInfoHash()
public function getInfoHash(): string
{
return $this->infoHash;
return $this->getInfoHashAsHex();
}
/** Returns torrent info hash as HEX string */
/**
* @deprecated Use getInfoHash() instead
*
* Returns torrent info hash as HEX string
*/
public function getInfoHashAsHex(): string
{
if (null === $this->infoHashHexCache) {

View File

@ -68,6 +68,15 @@
"pagerfanta/pagerfanta": {
"version": "v2.0.1"
},
"phpdocumentor/reflection-common": {
"version": "1.0.1"
},
"phpdocumentor/reflection-docblock": {
"version": "4.3.0"
},
"phpdocumentor/type-resolver": {
"version": "0.4.0"
},
"psr/cache": {
"version": "1.0.1"
},
@ -170,6 +179,9 @@
"symfony/property-access": {
"version": "v4.1.0"
},
"symfony/property-info": {
"version": "v4.1.0"
},
"symfony/routing": {
"version": "4.0",
"recipe": {
@ -179,6 +191,12 @@
"ref": "cda8b550123383d25827705d05a42acf6819fe4e"
}
},
"symfony/serializer": {
"version": "v4.1.0"
},
"symfony/serializer-pack": {
"version": "v1.0.1"
},
"symfony/translation": {
"version": "3.3",
"recipe": {
@ -227,6 +245,9 @@
"twig/twig": {
"version": "v2.4.8"
},
"webmozart/assert": {
"version": "1.3.0"
},
"white-october/pagerfanta-bundle": {
"version": "v1.2.1"
},

View File

@ -31,7 +31,7 @@
<a class="nav-link" href="#">Item</a>
</li>-->
</ul>
<form class="form-inline my-2 my-lg-0" action="{{ path('torrent_search') }}" method="get">
<form class="form-inline my-2 my-lg-0" action="{{ path('torrents_search') }}" method="get">
<input name="query" class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search"
value="{% if searchQuery is defined %}{{ searchQuery }}{% endif %}">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>

View File

@ -17,7 +17,7 @@
{% for torrent in torrents %}
<tr>
<td><a href="{{ magnet(torrent.name, torrent.infoHashAsHex) }}">&#128279;</a></td>
<td><a href="{{ path('torrent_show', {'id': torrent.id}) }}">{{ torrent.name }}</a></td>
<td><a href="{{ path('torrents_show', {'id': torrent.id}) }}">{{ torrent.name }}</a></td>
<td>{{ torrent.totalSize | readable_size }}</td>
<td>{{ torrent.discoveredOn | date('Y-m-d H:i:s')}}</td>
</tr>