From 2d6d35a398c099405859132e1f2cd8241fb3f317 Mon Sep 17 00:00:00 2001
From: Alejandro Celaya <alejandro@alejandrocelaya.com>
Date: Fri, 10 Aug 2018 23:14:45 +0200
Subject: [PATCH] Added shortUrl field to serialized ShortUrl objects, both
 from CLI and REST

---
 composer.json                                 |  1 +
 module/CLI/config/dependencies.config.php     |  6 ++-
 .../Shortcode/ListShortcodesCommand.php       | 28 ++++++----
 .../Shortcode/ListShortcodesCommandTest.php   |  6 +--
 .../Paginator/Util/PaginatorUtilsTrait.php    | 12 +++--
 .../src/Rest/DataTransformerInterface.php     |  9 ++++
 module/Core/src/Entity/ShortUrl.php           | 47 ++++++++--------
 module/Core/src/Service/UrlShortener.php      |  2 +-
 .../Transformer/ShortUrlDataTransformer.php   | 54 +++++++++++++++++++
 module/Rest/config/dependencies.config.php    |  7 ++-
 .../Action/ShortCode/ListShortCodesAction.php | 11 +++-
 .../ShortCode/ListShortCodesActionTest.php    |  5 +-
 12 files changed, 144 insertions(+), 44 deletions(-)
 create mode 100644 module/Common/src/Rest/DataTransformerInterface.php
 create mode 100644 module/Core/src/Transformer/ShortUrlDataTransformer.php

diff --git a/composer.json b/composer.json
index d12596d7..9d46a882 100644
--- a/composer.json
+++ b/composer.json
@@ -13,6 +13,7 @@
     ],
     "require": {
         "php": "^7.1",
+        "ext-json": "*",
         "acelaya/ze-content-based-error-handler": "^2.2",
         "cocur/slugify": "^3.0",
         "doctrine/cache": "^1.6",
diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php
index 4dd461be..236b8d87 100644
--- a/module/CLI/config/dependencies.config.php
+++ b/module/CLI/config/dependencies.config.php
@@ -42,7 +42,11 @@ return [
             'config.url_shortener.domain',
         ],
         Command\Shortcode\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
-        Command\Shortcode\ListShortcodesCommand::class => [Service\ShortUrlService::class, 'translator'],
+        Command\Shortcode\ListShortcodesCommand::class => [
+            Service\ShortUrlService::class,
+            'translator',
+            'config.url_shortener.domain',
+        ],
         Command\Shortcode\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
         Command\Shortcode\GeneratePreviewCommand::class => [
             Service\ShortUrlService::class,
diff --git a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php
index 1e8ce12c..f0e9efa3 100644
--- a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php
+++ b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Shortcode;
 use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
 use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
 use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
+use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -27,15 +28,23 @@ class ListShortcodesCommand extends Command
      * @var TranslatorInterface
      */
     private $translator;
+    /**
+     * @var array
+     */
+    private $domainConfig;
 
-    public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
-    {
+    public function __construct(
+        ShortUrlServiceInterface $shortUrlService,
+        TranslatorInterface $translator,
+        array $domainConfig
+    ) {
         $this->shortUrlService = $shortUrlService;
         $this->translator = $translator;
         parent::__construct();
+        $this->domainConfig = $domainConfig;
     }
 
-    public function configure()
+    protected function configure(): void
     {
         $this->setName(self::NAME)
              ->setDescription($this->translator->translate('List all short URLs'))
@@ -79,7 +88,7 @@ class ListShortcodesCommand extends Command
              );
     }
 
-    public function execute(InputInterface $input, OutputInterface $output)
+    protected function execute(InputInterface $input, OutputInterface $output)
     {
         $io = new SymfonyStyle($input, $output);
         $page = (int) $input->getOption('page');
@@ -87,6 +96,7 @@ class ListShortcodesCommand extends Command
         $tags = $input->getOption('tags');
         $tags = ! empty($tags) ? \explode(',', $tags) : [];
         $showTags = $input->getOption('showTags');
+        $transformer = new ShortUrlDataTransformer($this->domainConfig);
 
         do {
             $result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
@@ -94,7 +104,8 @@ class ListShortcodesCommand extends Command
 
             $headers = [
                 $this->translator->translate('Short code'),
-                $this->translator->translate('Original URL'),
+                $this->translator->translate('Short URL'),
+                $this->translator->translate('Long URL'),
                 $this->translator->translate('Date created'),
                 $this->translator->translate('Visits count'),
             ];
@@ -104,17 +115,14 @@ class ListShortcodesCommand extends Command
 
             $rows = [];
             foreach ($result as $row) {
-                $shortUrl = $row->jsonSerialize();
+                $shortUrl = $transformer->transform($row);
                 if ($showTags) {
-                    $shortUrl['tags'] = [];
-                    foreach ($row->getTags() as $tag) {
-                        $shortUrl['tags'][] = $tag->getName();
-                    }
                     $shortUrl['tags'] = implode(', ', $shortUrl['tags']);
                 } else {
                     unset($shortUrl['tags']);
                 }
 
+                unset($shortUrl['originalUrl']);
                 $rows[] = \array_values($shortUrl);
             }
             $io->table($headers, $rows);
diff --git a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php
index 8aede93a..c79a2bb1 100644
--- a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php
+++ b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php
@@ -30,7 +30,7 @@ class ListShortcodesCommandTest extends TestCase
     {
         $this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
         $app = new Application();
-        $command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]));
+        $command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]), []);
         $app->add($command);
         $this->commandTester = new CommandTester($command);
     }
@@ -55,7 +55,7 @@ class ListShortcodesCommandTest extends TestCase
         // The paginator will return more than one page for the first 3 times
         $data = [];
         for ($i = 0; $i < 50; $i++) {
-            $data[] = new ShortUrl();
+            $data[] = (new ShortUrl())->setLongUrl('url_' . $i);
         }
 
         $this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use (&$data) {
@@ -74,7 +74,7 @@ class ListShortcodesCommandTest extends TestCase
         // The paginator will return more than one page
         $data = [];
         for ($i = 0; $i < 30; $i++) {
-            $data[] = new ShortUrl();
+            $data[] = (new ShortUrl())->setLongUrl('url_' . $i);
         }
 
         $this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data)))
diff --git a/module/Common/src/Paginator/Util/PaginatorUtilsTrait.php b/module/Common/src/Paginator/Util/PaginatorUtilsTrait.php
index 40f51d90..1b01de42 100644
--- a/module/Common/src/Paginator/Util/PaginatorUtilsTrait.php
+++ b/module/Common/src/Paginator/Util/PaginatorUtilsTrait.php
@@ -3,15 +3,16 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\Common\Paginator\Util;
 
+use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
 use Zend\Paginator\Paginator;
 use Zend\Stdlib\ArrayUtils;
 
 trait PaginatorUtilsTrait
 {
-    protected function serializePaginator(Paginator $paginator): array
+    private function serializePaginator(Paginator $paginator, ?DataTransformerInterface $transformer = null): array
     {
         return [
-            'data' => ArrayUtils::iteratorToArray($paginator->getCurrentItems()),
+            'data' => $this->serializeItems(ArrayUtils::iteratorToArray($paginator->getCurrentItems()), $transformer),
             'pagination' => [
                 'currentPage' => $paginator->getCurrentPageNumber(),
                 'pagesCount' => $paginator->count(),
@@ -22,13 +23,18 @@ trait PaginatorUtilsTrait
         ];
     }
 
+    private function serializeItems(array $items, ?DataTransformerInterface $transformer = null): array
+    {
+        return $transformer === null ? $items : \array_map([$transformer, 'transform'], $items);
+    }
+
     /**
      * Checks if provided paginator is in last page
      *
      * @param Paginator $paginator
      * @return bool
      */
-    protected function isLastPage(Paginator $paginator): bool
+    private function isLastPage(Paginator $paginator): bool
     {
         return $paginator->getCurrentPageNumber() >= $paginator->count();
     }
diff --git a/module/Common/src/Rest/DataTransformerInterface.php b/module/Common/src/Rest/DataTransformerInterface.php
new file mode 100644
index 00000000..933f6cce
--- /dev/null
+++ b/module/Common/src/Rest/DataTransformerInterface.php
@@ -0,0 +1,9 @@
+<?php
+declare(strict_types=1);
+
+namespace Shlinkio\Shlink\Common\Rest;
+
+interface DataTransformerInterface
+{
+    public function transform($value): array;
+}
diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php
index ff21c47b..def81f01 100644
--- a/module/Core/src/Entity/ShortUrl.php
+++ b/module/Core/src/Entity/ShortUrl.php
@@ -16,7 +16,7 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity;
  * @ORM\Entity(repositoryClass="Shlinkio\Shlink\Core\Repository\ShortUrlRepository")
  * @ORM\Table(name="short_urls")
  */
-class ShortUrl extends AbstractEntity implements \JsonSerializable
+class ShortUrl extends AbstractEntity
 {
     /**
      * @var string
@@ -84,21 +84,40 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
     /**
      * @return string
      */
-    public function getOriginalUrl(): string
+    public function getLongUrl(): string
     {
         return $this->originalUrl;
     }
 
     /**
-     * @param string $originalUrl
+     * @param string $longUrl
      * @return $this
      */
-    public function setOriginalUrl(string $originalUrl)
+    public function setLongUrl(string $longUrl): self
     {
-        $this->originalUrl = $originalUrl;
+        $this->originalUrl = $longUrl;
         return $this;
     }
 
+    /**
+     * @return string
+     * @deprecated Use getLongUrl() instead
+     */
+    public function getOriginalUrl(): string
+    {
+        return $this->getLongUrl();
+    }
+
+    /**
+     * @param string $originalUrl
+     * @return $this
+     * @deprecated Use setLongUrl() instead
+     */
+    public function setOriginalUrl(string $originalUrl): self
+    {
+        return $this->setLongUrl($originalUrl);
+    }
+
     /**
      * @return string
      */
@@ -237,22 +256,4 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
     {
         return $this->maxVisits !== null && $this->getVisitsCount() >= $this->maxVisits;
     }
-
-    /**
-     * Specify data which should be serialized to JSON
-     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
-     * @return mixed data which can be serialized by <b>json_encode</b>,
-     * which is a value of any type other than a resource.
-     * @since 5.4.0
-     */
-    public function jsonSerialize()
-    {
-        return [
-            'shortCode' => $this->shortCode,
-            'originalUrl' => $this->originalUrl,
-            'dateCreated' => $this->dateCreated !== null ? $this->dateCreated->format(\DateTime::ATOM) : null,
-            'visitsCount' => $this->getVisitsCount(),
-            'tags' => $this->tags->toArray(),
-        ];
-    }
 }
diff --git a/module/Core/src/Service/UrlShortener.php b/module/Core/src/Service/UrlShortener.php
index 87621060..e1696adc 100644
--- a/module/Core/src/Service/UrlShortener.php
+++ b/module/Core/src/Service/UrlShortener.php
@@ -23,7 +23,7 @@ class UrlShortener implements UrlShortenerInterface
 {
     use TagManagerTrait;
 
-    const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ';
+    public const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ';
 
     /**
      * @var ClientInterface
diff --git a/module/Core/src/Transformer/ShortUrlDataTransformer.php b/module/Core/src/Transformer/ShortUrlDataTransformer.php
new file mode 100644
index 00000000..c1113a81
--- /dev/null
+++ b/module/Core/src/Transformer/ShortUrlDataTransformer.php
@@ -0,0 +1,54 @@
+<?php
+declare(strict_types=1);
+
+namespace Shlinkio\Shlink\Core\Transformer;
+
+use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
+use Shlinkio\Shlink\Core\Entity\ShortUrl;
+use Shlinkio\Shlink\Core\Entity\Tag;
+
+class ShortUrlDataTransformer implements DataTransformerInterface
+{
+    /**
+     * @var array
+     */
+    private $domainConfig;
+
+    public function __construct(array $domainConfig)
+    {
+        $this->domainConfig = $domainConfig;
+    }
+
+    /**
+     * @param ShortUrl $value
+     * @return array
+     */
+    public function transform($value): array
+    {
+        $dateCreated = $value->getDateCreated();
+        $longUrl = $value->getLongUrl();
+        $shortCode = $value->getShortCode();
+
+        return [
+            'shortCode' => $shortCode,
+            'shortUrl' => \sprintf(
+                '%s://%s/%s',
+                $this->domainConfig['schema'] ?? 'http',
+                $this->domainConfig['hostname'] ?? '',
+                $shortCode
+            ),
+            'longUrl' => $longUrl,
+            'dateCreated' => $dateCreated !== null ? $dateCreated->format(\DateTime::ATOM) : null,
+            'visitsCount' => $value->getVisitsCount(),
+            'tags' => \array_map([$this, 'serializeTag'], $value->getTags()->toArray()),
+
+            // Deprecated
+            'originalUrl' => $longUrl,
+        ];
+    }
+
+    private function serializeTag(Tag $tag): string
+    {
+        return $tag->getName();
+    }
+}
diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php
index 304e667d..42147707 100644
--- a/module/Rest/config/dependencies.config.php
+++ b/module/Rest/config/dependencies.config.php
@@ -61,7 +61,12 @@ return [
         Action\ShortCode\EditShortCodeAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink',],
         Action\ShortCode\ResolveUrlAction::class => [Service\UrlShortener::class, 'translator'],
         Action\Visit\GetVisitsAction::class => [Service\VisitsTracker::class, 'translator', 'Logger_Shlink'],
-        Action\ShortCode\ListShortCodesAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink'],
+        Action\ShortCode\ListShortCodesAction::class => [
+            Service\ShortUrlService::class,
+            'translator',
+            'config.url_shortener.domain',
+            'Logger_Shlink',
+        ],
         Action\ShortCode\EditShortCodeTagsAction::class => [
             Service\ShortUrlService::class,
             'translator',
diff --git a/module/Rest/src/Action/ShortCode/ListShortCodesAction.php b/module/Rest/src/Action/ShortCode/ListShortCodesAction.php
index 58f26d77..e69840a5 100644
--- a/module/Rest/src/Action/ShortCode/ListShortCodesAction.php
+++ b/module/Rest/src/Action/ShortCode/ListShortCodesAction.php
@@ -8,6 +8,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
 use Psr\Log\LoggerInterface;
 use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
 use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
+use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
 use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
 use Shlinkio\Shlink\Rest\Util\RestUtils;
 use Zend\Diactoros\Response\JsonResponse;
@@ -28,15 +29,21 @@ class ListShortCodesAction extends AbstractRestAction
      * @var TranslatorInterface
      */
     private $translator;
+    /**
+     * @var array
+     */
+    private $domainConfig;
 
     public function __construct(
         ShortUrlServiceInterface $shortUrlService,
         TranslatorInterface $translator,
+        array $domainConfig,
         LoggerInterface $logger = null
     ) {
         parent::__construct($logger);
         $this->shortUrlService = $shortUrlService;
         $this->translator = $translator;
+        $this->domainConfig = $domainConfig;
     }
 
     /**
@@ -49,7 +56,9 @@ class ListShortCodesAction extends AbstractRestAction
         try {
             $params = $this->queryToListParams($request->getQueryParams());
             $shortUrls = $this->shortUrlService->listShortUrls(...$params);
-            return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls)]);
+            return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls, new ShortUrlDataTransformer(
+                $this->domainConfig
+            ))]);
         } catch (\Exception $e) {
             $this->logger->error('Unexpected error while listing short URLs.' . PHP_EOL . $e);
             return new JsonResponse([
diff --git a/module/Rest/test/Action/ShortCode/ListShortCodesActionTest.php b/module/Rest/test/Action/ShortCode/ListShortCodesActionTest.php
index 59912f5a..7c414e44 100644
--- a/module/Rest/test/Action/ShortCode/ListShortCodesActionTest.php
+++ b/module/Rest/test/Action/ShortCode/ListShortCodesActionTest.php
@@ -26,7 +26,10 @@ class ListShortCodesActionTest extends TestCase
     public function setUp()
     {
         $this->service = $this->prophesize(ShortUrlService::class);
-        $this->action = new ListShortCodesAction($this->service->reveal(), Translator::factory([]));
+        $this->action = new ListShortCodesAction($this->service->reveal(), Translator::factory([]), [
+            'hostname' => 'doma.in',
+            'schema' => 'https',
+        ]);
     }
 
     /**