From 027476c5530bd80893475c859b77ae983c09f701 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Fri, 22 Jun 2018 22:45:17 +0300 Subject: [PATCH] Simple API V1 implemented using Symfony Serializer. --- composer.json | 1 + composer.lock | 391 +++++++++++++++++++- config/routes.yaml | 31 +- config/services.yaml | 7 + src/Api/V1/Controller/TorrentController.php | 46 +++ src/Api/V1/DTO/ApiResponse.php | 88 +++++ src/Controller/MainController.php | 3 +- src/Controller/TorrentController.php | 17 +- src/Entity/File.php | 5 + src/Entity/Torrent.php | 27 +- symfony.lock | 21 ++ templates/base.html.twig | 2 +- templates/torrent_list.html.twig | 2 +- 13 files changed, 618 insertions(+), 23 deletions(-) create mode 100644 src/Api/V1/Controller/TorrentController.php create mode 100644 src/Api/V1/DTO/ApiResponse.php diff --git a/composer.json b/composer.json index 3d6c4d5..2fd1c0b 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 9547b3e..a00092e 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/routes.yaml b/config/routes.yaml index a82b509..5060052 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -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 \ No newline at end of file + 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+' \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index 6aff2bd..4e0021b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -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 } \ No newline at end of file diff --git a/src/Api/V1/Controller/TorrentController.php b/src/Api/V1/Controller/TorrentController.php new file mode 100644 index 0000000..dc6303a --- /dev/null +++ b/src/Api/V1/Controller/TorrentController.php @@ -0,0 +1,46 @@ +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']), + ]); + } + + + + +} \ No newline at end of file diff --git a/src/Api/V1/DTO/ApiResponse.php b/src/Api/V1/DTO/ApiResponse.php new file mode 100644 index 0000000..b31a6ed --- /dev/null +++ b/src/Api/V1/DTO/ApiResponse.php @@ -0,0 +1,88 @@ +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; + } +} \ No newline at end of file diff --git a/src/Controller/MainController.php b/src/Controller/MainController.php index 096e20f..1d22536 100644 --- a/src/Controller/MainController.php +++ b/src/Controller/MainController.php @@ -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(), diff --git a/src/Controller/TorrentController.php b/src/Controller/TorrentController.php index bd55e21..1e7b1bd 100644 --- a/src/Controller/TorrentController.php +++ b/src/Controller/TorrentController.php @@ -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, + ]); + } } \ No newline at end of file diff --git a/src/Entity/File.php b/src/Entity/File.php index 452558c..5d04e27 100644 --- a/src/Entity/File.php +++ b/src/Entity/File.php @@ -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; diff --git a/src/Entity/Torrent.php b/src/Entity/Torrent.php index 63650c5..eb9c601 100644 --- a/src/Entity/Torrent.php +++ b/src/Entity/Torrent.php @@ -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) { diff --git a/symfony.lock b/symfony.lock index e4dca27..f464506 100644 --- a/symfony.lock +++ b/symfony.lock @@ -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" }, diff --git a/templates/base.html.twig b/templates/base.html.twig index 6d3ab23..16e1876 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -31,7 +31,7 @@ Item --> -
+ diff --git a/templates/torrent_list.html.twig b/templates/torrent_list.html.twig index 57c8d9c..d0b4326 100644 --- a/templates/torrent_list.html.twig +++ b/templates/torrent_list.html.twig @@ -17,7 +17,7 @@ {% for torrent in torrents %} 🔗 - {{ torrent.name }} + {{ torrent.name }} {{ torrent.totalSize | readable_size }} {{ torrent.discoveredOn | date('Y-m-d H:i:s')}}