mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
Merge branch 'develop'
This commit is contained in:
commit
fb9d3268ad
47 changed files with 1506 additions and 237 deletions
10
.env.dist
Normal file
10
.env.dist
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Application
|
||||
APP_ENV=
|
||||
SHORTENED_URL_SCHEMA=
|
||||
SHORTENED_URL_HOSTNAME=
|
||||
SHORTCODE_CHARS=
|
||||
|
||||
# Database
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_NAME=
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
build
|
||||
composer.lock
|
||||
vendor/
|
||||
.env
|
||||
|
|
6
.scrutinizer.yml
Normal file
6
.scrutinizer.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
tools:
|
||||
external_code_coverage: true
|
||||
checks:
|
||||
php:
|
||||
code_rating: true
|
||||
duplication: true
|
40
.travis.yml
40
.travis.yml
|
@ -1,28 +1,26 @@
|
|||
sudo: false
|
||||
|
||||
language: php
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- php: 5.5
|
||||
- php: 5.6
|
||||
env:
|
||||
- EXECUTE_CS_CHECK=true
|
||||
- php: 7
|
||||
- php: hhvm
|
||||
allow_failures:
|
||||
- php: hhvm
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
|
||||
before_install:
|
||||
php:
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7
|
||||
- hhvm
|
||||
|
||||
before_script:
|
||||
- composer self-update
|
||||
|
||||
install:
|
||||
- travis_retry composer install --no-interaction --ignore-platform-reqs --prefer-source --no-scripts
|
||||
- composer install --no-interaction
|
||||
|
||||
script:
|
||||
- composer test
|
||||
- if [[ $EXECUTE_CS_CHECK == 'true' ]]; then composer cs ; fi
|
||||
- mkdir build
|
||||
- composer check
|
||||
|
||||
notifications:
|
||||
email: true
|
||||
after_script:
|
||||
- wget https://scrutinizer-ci.com/ocular.phar
|
||||
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
|
||||
|
||||
sudo: false
|
||||
|
|
5
bin/cli
Normal file → Executable file
5
bin/cli
Normal file → Executable file
|
@ -1,11 +1,14 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\Expressive\Application;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/../config/container.php';
|
||||
/** @var Application $app */
|
||||
$app = include __DIR__ . '/../config/app.php';
|
||||
$app = $container->get(Application::class);
|
||||
|
||||
$command = count($_SERVER['argv']) > 1 ? $_SERVER['argv'][1] : '';
|
||||
$request = ServerRequestFactory::fromGlobals()
|
||||
|
|
11
cli-config.php
Normal file
11
cli-config.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Interop\Container\ContainerInterface;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/config/container.php';
|
||||
/** @var EntityManager $em */
|
||||
$em = $container->get(EntityManager::class);
|
||||
|
||||
return ConsoleRunner::createHelperSet($em);
|
|
@ -14,18 +14,21 @@
|
|||
"php": "^5.5 || ^7.0",
|
||||
"zendframework/zend-expressive": "^1.0",
|
||||
"zendframework/zend-expressive-helpers": "^2.0",
|
||||
"zendframework/zend-stdlib": "^2.7",
|
||||
"zendframework/zend-expressive-aurarouter": "^1.0",
|
||||
"zendframework/zend-servicemanager": "^3.0",
|
||||
"zendframework/zend-expressive-fastroute": "^1.1",
|
||||
"zendframework/zend-expressive-twigrenderer": "^1.0",
|
||||
"acelaya/expressive-slim-router": "^2.0"
|
||||
"zendframework/zend-stdlib": "^2.7",
|
||||
"zendframework/zend-servicemanager": "^3.0",
|
||||
"doctrine/orm": "^2.5",
|
||||
"guzzlehttp/guzzle": "^6.2",
|
||||
"acelaya/zsm-annotated-services": "^0.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8",
|
||||
"squizlabs/php_codesniffer": "^2.3",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"filp/whoops": "^2.0",
|
||||
"symfony/var-dumper": "^3.0"
|
||||
"symfony/var-dumper": "^3.0",
|
||||
"vlucas/phpdotenv": "^2.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@ -45,7 +48,7 @@
|
|||
"cs": "phpcs",
|
||||
"cs-fix": "phpcbf",
|
||||
"serve": "php -S 0.0.0.0:8000 -t public/",
|
||||
"test": "phpunit",
|
||||
"pretty-test": "phpunit -c tests/phpunit.xml --coverage-html build/coverage"
|
||||
"test": "phpunit --coverage-clover build/clover.xml",
|
||||
"pretty-test": "phpunit --coverage-html build/coverage"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<?php
|
||||
use Zend\Expressive\Application;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// Build container
|
||||
$config = require __DIR__ . '/config.php';
|
||||
$container = new ServiceManager($config['services']);
|
||||
$container->setService('config', $config);
|
||||
|
||||
return $container->get(Application::class);
|
15
config/autoload/cli-routes.global.php
Normal file
15
config/autoload/cli-routes.global.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
use Acelaya\UrlShortener\Middleware\CliRoutable;
|
||||
|
||||
return [
|
||||
|
||||
'routes' => [
|
||||
[
|
||||
'name' => 'cli-generate-shortcode',
|
||||
'path' => '/generate-shortcode',
|
||||
'middleware' => CliRoutable\GenerateShortcodeMiddleware::class,
|
||||
'allowed_methods' => ['CLI'],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
15
config/autoload/database.global.php
Normal file
15
config/autoload/database.global.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
return [
|
||||
|
||||
'database' => [
|
||||
'driver' => 'pdo_mysql',
|
||||
'user' => getenv('DB_USER'),
|
||||
'password' => getenv('DB_PASSWORD'),
|
||||
'dbname' => getenv('DB_NAME') ?: 'acelaya_url_shortener',
|
||||
'charset' => 'utf8',
|
||||
'driverOptions' => [
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
|
||||
],
|
||||
],
|
||||
|
||||
];
|
|
@ -1,9 +1,10 @@
|
|||
<?php
|
||||
use Acelaya\UrlShortener\Middleware\CliParamsMiddleware;
|
||||
use Zend\Expressive\Container\ApplicationFactory;
|
||||
use Zend\Expressive\Helper;
|
||||
|
||||
return [
|
||||
|
||||
|
||||
'middleware_pipeline' => [
|
||||
'always' => [
|
||||
'middleware' => [
|
||||
|
@ -15,6 +16,7 @@ return [
|
|||
'routing' => [
|
||||
'middleware' => [
|
||||
ApplicationFactory::ROUTING_MIDDLEWARE,
|
||||
CliParamsMiddleware::class,
|
||||
Helper\UrlHelperMiddleware::class,
|
||||
ApplicationFactory::DISPATCH_MIDDLEWARE,
|
||||
],
|
||||
|
|
|
@ -1,26 +1,15 @@
|
|||
<?php
|
||||
use Acelaya\UrlShortener\Middleware\Routable;
|
||||
|
||||
return [
|
||||
|
||||
'routes' => [
|
||||
[
|
||||
'name' => 'home',
|
||||
'path' => '/',
|
||||
'middleware' => function ($req, $resp) {
|
||||
$resp->getBody()->write('Hello world');
|
||||
return $resp;
|
||||
},
|
||||
'name' => 'long-url-redirect',
|
||||
'path' => '/{shortCode}',
|
||||
'middleware' => Routable\RedirectMiddleware::class,
|
||||
'allowed_methods' => ['GET'],
|
||||
],
|
||||
[
|
||||
'name' => 'cli',
|
||||
'path' => '/command-name',
|
||||
'middleware' => function ($req, $resp) {
|
||||
$resp->getBody()->write('Hello world from cli');
|
||||
return $resp;
|
||||
},
|
||||
'allowed_methods' => ['CLI'],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
];
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
<?php
|
||||
use Acelaya\UrlShortener\Factory\CacheFactory;
|
||||
use Acelaya\UrlShortener\Factory\EntityManagerFactory;
|
||||
use Acelaya\UrlShortener\Middleware;
|
||||
use Acelaya\UrlShortener\Service;
|
||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Zend\Expressive\Application;
|
||||
use Zend\Expressive\Container;
|
||||
use Zend\Expressive\Helper;
|
||||
use Zend\Expressive\Router;
|
||||
use Zend\Expressive\Template;
|
||||
use Zend\Expressive\Twig;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'services' => [
|
||||
'invokables' => [
|
||||
Helper\ServerUrlHelper::class => Helper\ServerUrlHelper::class,
|
||||
Router\RouterInterface::class => Router\AuraRouter::class,
|
||||
],
|
||||
'factories' => [
|
||||
Application::class => Container\ApplicationFactory::class,
|
||||
|
||||
|
@ -19,11 +24,30 @@ return [
|
|||
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
|
||||
Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
|
||||
Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
|
||||
Helper\ServerUrlHelper::class => InvokableFactory::class,
|
||||
Router\FastRouteRouter::class => InvokableFactory::class,
|
||||
|
||||
// View
|
||||
'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class,
|
||||
Template\TemplateRendererInterface::class => Zend\Expressive\Twig\TwigRendererFactory::class,
|
||||
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
|
||||
|
||||
// Services
|
||||
EntityManager::class => EntityManagerFactory::class,
|
||||
GuzzleHttp\Client::class => InvokableFactory::class,
|
||||
Service\UrlShortener::class => AnnotatedFactory::class,
|
||||
Service\VisitsTracker::class => AnnotatedFactory::class,
|
||||
Cache::class => CacheFactory::class,
|
||||
|
||||
// Middleware
|
||||
Middleware\CliRoutable\GenerateShortcodeMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\CliParamsMiddleware::class => Middleware\Factory\CliParamsMiddlewareFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'em' => EntityManager::class,
|
||||
'httpClient' => GuzzleHttp\Client::class,
|
||||
Router\RouterInterface::class => Router\FastRouteRouter::class,
|
||||
]
|
||||
],
|
||||
|
||||
];
|
||||
|
|
12
config/autoload/url-shortener.global.php
Normal file
12
config/autoload/url-shortener.global.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => getenv('SHORTENED_URL_SCHEMA') ?: 'http',
|
||||
'hostname' => getenv('SHORTENED_URL_HOSTNAME'),
|
||||
],
|
||||
'shortcode_chars' => getenv('SHORTCODE_CHARS'),
|
||||
],
|
||||
|
||||
];
|
|
@ -29,6 +29,4 @@ if (is_file($cachedConfigFile)) {
|
|||
}
|
||||
}
|
||||
|
||||
// Return an ArrayObject so we can inject the config as a service in Aura.Di
|
||||
// and still use array checks like ``is_array``.
|
||||
return new ArrayObject($config, ArrayObject::ARRAY_AS_PROPS);
|
||||
return $config;
|
||||
|
|
21
config/container.php
Normal file
21
config/container.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
use Dotenv\Dotenv;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
// If the Dotenv class exists, load env vars and enable errors
|
||||
if (class_exists(Dotenv::class)) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
$dotenv = new Dotenv(__DIR__ . '/..');
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
// Build container
|
||||
$config = require __DIR__ . '/config.php';
|
||||
$container = new ServiceManager($config['services']);
|
||||
$container->setService('config', $config);
|
||||
return $container;
|
0
data/.gitignore → data/cache/.gitignore
vendored
0
data/.gitignore → data/cache/.gitignore
vendored
2
data/proxies/.gitignore
vendored
Normal file
2
data/proxies/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -1,7 +1,7 @@
|
|||
<phpunit bootstrap="./vendor/autoload.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="App\\Tests">
|
||||
<directory>./test</directory>
|
||||
<testsuite name="AcelayaTest">
|
||||
<directory>./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<?php
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Zend\Expressive\Application;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/../config/container.php';
|
||||
/** @var Application $app */
|
||||
$app = include __DIR__ . '/../config/app.php';
|
||||
$app = $container->get(Application::class);
|
||||
$app->run();
|
||||
|
|
33
src/Entity/AbstractEntity.php
Normal file
33
src/Entity/AbstractEntity.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
abstract class AbstractEntity
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* @ORM\Column(name="id", type="bigint", options={"unsigned"=true})
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return $this
|
||||
*/
|
||||
public function setId($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
}
|
120
src/Entity/ShortUrl.php
Normal file
120
src/Entity/ShortUrl.php
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Class ShortUrl
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="short_urls")
|
||||
*/
|
||||
class ShortUrl extends AbstractEntity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(name="original_url", type="string", nullable=false, length=1024, unique=true)
|
||||
*/
|
||||
protected $originalUrl;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(name="short_code", type="string", nullable=false, length=10, unique=true)
|
||||
*/
|
||||
protected $shortCode;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(name="date_created", type="datetime")
|
||||
*/
|
||||
protected $dateCreated;
|
||||
/**
|
||||
* @var Collection|Visit[]
|
||||
* @ORM\OneToMany(targetEntity=Visit::class, mappedBy="shortUrl", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
protected $visits;
|
||||
|
||||
/**
|
||||
* ShortUrl constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDateCreated(new \DateTime());
|
||||
$this->setVisits(new ArrayCollection());
|
||||
$this->setShortCode('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOriginalUrl()
|
||||
{
|
||||
return $this->originalUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $originalUrl
|
||||
* @return $this
|
||||
*/
|
||||
public function setOriginalUrl($originalUrl)
|
||||
{
|
||||
$this->originalUrl = (string) $originalUrl;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getShortCode()
|
||||
{
|
||||
return $this->shortCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @return $this
|
||||
*/
|
||||
public function setShortCode($shortCode)
|
||||
{
|
||||
$this->shortCode = $shortCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
return $this->dateCreated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateCreated
|
||||
* @return $this
|
||||
*/
|
||||
public function setDateCreated($dateCreated)
|
||||
{
|
||||
$this->dateCreated = $dateCreated;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Visit[]|Collection
|
||||
*/
|
||||
public function getVisits()
|
||||
{
|
||||
return $this->visits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Visit[]|Collection $visits
|
||||
* @return $this
|
||||
*/
|
||||
public function setVisits($visits)
|
||||
{
|
||||
$this->visits = $visits;
|
||||
return $this;
|
||||
}
|
||||
}
|
137
src/Entity/Visit.php
Normal file
137
src/Entity/Visit.php
Normal file
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Class Visit
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="visits")
|
||||
*/
|
||||
class Visit extends AbstractEntity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(type="string", length=256, nullable=true)
|
||||
*/
|
||||
protected $referer;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(type="datetime", nullable=false)
|
||||
*/
|
||||
protected $date;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(type="string", length=256, name="remote_addr", nullable=true)
|
||||
*/
|
||||
protected $remoteAddr;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(type="string", length=256, name="user_agent", nullable=true)
|
||||
*/
|
||||
protected $userAgent;
|
||||
/**
|
||||
* @var ShortUrl
|
||||
* @ORM\ManyToOne(targetEntity=ShortUrl::class)
|
||||
* @ORM\JoinColumn(name="short_url_id", referencedColumnName="id")
|
||||
*/
|
||||
protected $shortUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->date = new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getReferer()
|
||||
{
|
||||
return $this->referer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $referer
|
||||
* @return $this
|
||||
*/
|
||||
public function setReferer($referer)
|
||||
{
|
||||
$this->referer = $referer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDate()
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
* @return $this
|
||||
*/
|
||||
public function setDate($date)
|
||||
{
|
||||
$this->date = $date;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ShortUrl
|
||||
*/
|
||||
public function getShortUrl()
|
||||
{
|
||||
return $this->shortUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShortUrl $shortUrl
|
||||
* @return $this
|
||||
*/
|
||||
public function setShortUrl($shortUrl)
|
||||
{
|
||||
$this->shortUrl = $shortUrl;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteAddr()
|
||||
{
|
||||
return $this->remoteAddr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $remoteAddr
|
||||
* @return $this
|
||||
*/
|
||||
public function setRemoteAddr($remoteAddr)
|
||||
{
|
||||
$this->remoteAddr = $remoteAddr;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUserAgent()
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $userAgent
|
||||
* @return $this
|
||||
*/
|
||||
public function setUserAgent($userAgent)
|
||||
{
|
||||
$this->userAgent = $userAgent;
|
||||
return $this;
|
||||
}
|
||||
}
|
6
src/Exception/ExceptionInterface.php
Normal file
6
src/Exception/ExceptionInterface.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Exception;
|
||||
|
||||
interface ExceptionInterface
|
||||
{
|
||||
}
|
15
src/Exception/InvalidShortCodeException.php
Normal file
15
src/Exception/InvalidShortCodeException.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Exception;
|
||||
|
||||
class InvalidShortCodeException extends RuntimeException
|
||||
{
|
||||
public static function fromShortCode($shortCode, $charSet, \Exception $previous = null)
|
||||
{
|
||||
$code = isset($previous) ? $previous->getCode() : -1;
|
||||
return new static(
|
||||
sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet),
|
||||
$code,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
11
src/Exception/InvalidUrlException.php
Normal file
11
src/Exception/InvalidUrlException.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Exception;
|
||||
|
||||
class InvalidUrlException extends RuntimeException
|
||||
{
|
||||
public static function fromUrl($url, \Exception $previous = null)
|
||||
{
|
||||
$code = isset($previous) ? $previous->getCode() : -1;
|
||||
return new static(sprintf('Provided URL "%s" is not an exisitng and valid URL', $url), $code, $previous);
|
||||
}
|
||||
}
|
6
src/Exception/RuntimeException.php
Normal file
6
src/Exception/RuntimeException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Exception;
|
||||
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
30
src/Factory/CacheFactory.php
Normal file
30
src/Factory/CacheFactory.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Factory;
|
||||
|
||||
use Doctrine\Common\Cache\ApcuCache;
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class CacheFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
return getenv('APP_ENV') === 'pro' ? new ApcuCache() : new ArrayCache();
|
||||
}
|
||||
}
|
43
src/Factory/EntityManagerFactory.php
Normal file
43
src/Factory/EntityManagerFactory.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Factory;
|
||||
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Setup;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class EntityManagerFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$globalConfig = $container->get('config');
|
||||
$isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false;
|
||||
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
|
||||
$dbConfig = isset($globalConfig['database']) ? $globalConfig['database'] : [];
|
||||
|
||||
return EntityManager::create($dbConfig, Setup::createAnnotationMetadataConfiguration(
|
||||
['src/Entity'],
|
||||
$isDevMode,
|
||||
'data/proxies',
|
||||
$cache,
|
||||
false
|
||||
));
|
||||
}
|
||||
}
|
71
src/Middleware/CliParamsMiddleware.php
Normal file
71
src/Middleware/CliParamsMiddleware.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
class CliParamsMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $argv;
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
private $currentSapi;
|
||||
|
||||
public function __construct(array $argv, $currentSapi)
|
||||
{
|
||||
$this->argv = $argv;
|
||||
$this->currentSapi = $currentSapi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming request and/or response.
|
||||
*
|
||||
* Accepts a server-side request and a response instance, and does
|
||||
* something with them.
|
||||
*
|
||||
* If the response is not complete and/or further processing would not
|
||||
* interfere with the work done in the middleware, or if the middleware
|
||||
* wants to delegate to another process, it can use the `$out` callable
|
||||
* if present.
|
||||
*
|
||||
* If the middleware does not return a value, execution of the current
|
||||
* request is considered complete, and the response instance provided will
|
||||
* be considered the response to return.
|
||||
*
|
||||
* Alternately, the middleware may return a response instance.
|
||||
*
|
||||
* Often, middleware will `return $out();`, with the assumption that a
|
||||
* later middleware will return a response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param null|callable $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
// When not in CLI, just call next middleware
|
||||
if ($this->currentSapi !== 'cli') {
|
||||
return $out($request, $response);
|
||||
}
|
||||
|
||||
/** @var RouteResult $routeResult */
|
||||
$routeResult = $request->getAttribute(RouteResult::class);
|
||||
if (! $routeResult->isSuccess()) {
|
||||
return $out($request, $response);
|
||||
}
|
||||
|
||||
// Inject ARGV params as request attributes
|
||||
if ($routeResult->getMatchedRouteName() === 'cli-generate-shortcode') {
|
||||
$request = $request->withAttribute('longUrl', isset($this->argv[2]) ? $this->argv[2] : null);
|
||||
}
|
||||
|
||||
return $out($request, $response);
|
||||
}
|
||||
}
|
91
src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php
Normal file
91
src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware\CliRoutable;
|
||||
|
||||
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
||||
use Acelaya\UrlShortener\Service\UrlShortener;
|
||||
use Acelaya\UrlShortener\Service\UrlShortenerInterface;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
class GenerateShortcodeMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $domainConfig;
|
||||
|
||||
/**
|
||||
* GenerateShortcodeMiddleware constructor.
|
||||
*
|
||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
||||
* @param array $domainConfig
|
||||
*
|
||||
* @Inject({UrlShortener::class, "config.url_shortener.domain"})
|
||||
*/
|
||||
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
|
||||
{
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->domainConfig = $domainConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming request and/or response.
|
||||
*
|
||||
* Accepts a server-side request and a response instance, and does
|
||||
* something with them.
|
||||
*
|
||||
* If the response is not complete and/or further processing would not
|
||||
* interfere with the work done in the middleware, or if the middleware
|
||||
* wants to delegate to another process, it can use the `$out` callable
|
||||
* if present.
|
||||
*
|
||||
* If the middleware does not return a value, execution of the current
|
||||
* request is considered complete, and the response instance provided will
|
||||
* be considered the response to return.
|
||||
*
|
||||
* Alternately, the middleware may return a response instance.
|
||||
*
|
||||
* Often, middleware will `return $out();`, with the assumption that a
|
||||
* later middleware will return a response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param null|callable $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
$longUrl = $request->getAttribute('longUrl');
|
||||
|
||||
try {
|
||||
if (! isset($longUrl)) {
|
||||
$response->getBody()->write('A URL was not provided!' . PHP_EOL);
|
||||
return;
|
||||
}
|
||||
|
||||
$shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl));
|
||||
$shortUrl = (new Uri())->withPath($shortcode)
|
||||
->withScheme($this->domainConfig['schema'])
|
||||
->withHost($this->domainConfig['hostname']);
|
||||
|
||||
$response->getBody()->write(
|
||||
sprintf('Processed URL "%s".%sGenerated short URL "%s"', $longUrl, PHP_EOL, $shortUrl) . PHP_EOL
|
||||
);
|
||||
} catch (InvalidUrlException $e) {
|
||||
$response->getBody()->write(
|
||||
sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl) . PHP_EOL
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
$response->getBody()->write($e);
|
||||
} finally {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
32
src/Middleware/Factory/CliParamsMiddlewareFactory.php
Normal file
32
src/Middleware/Factory/CliParamsMiddlewareFactory.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware\Factory;
|
||||
|
||||
use Acelaya\UrlShortener\Middleware\CliParamsMiddleware;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class CliParamsMiddlewareFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
return new CliParamsMiddleware(
|
||||
isset($_SERVER['argv']) ? $_SERVER['argv'] : [],
|
||||
php_sapi_name()
|
||||
);
|
||||
}
|
||||
}
|
87
src/Middleware/Routable/RedirectMiddleware.php
Normal file
87
src/Middleware/Routable/RedirectMiddleware.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware\Routable;
|
||||
|
||||
use Acelaya\UrlShortener\Service\UrlShortener;
|
||||
use Acelaya\UrlShortener\Service\UrlShortenerInterface;
|
||||
use Acelaya\UrlShortener\Service\VisitsTracker;
|
||||
use Acelaya\UrlShortener\Service\VisitsTrackerInterface;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\RedirectResponse;
|
||||
use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
class RedirectMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var VisitsTracker|VisitsTrackerInterface
|
||||
*/
|
||||
private $visitTracker;
|
||||
|
||||
/**
|
||||
* RedirectMiddleware constructor.
|
||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
||||
* @param VisitsTrackerInterface|VisitsTracker $visitTracker
|
||||
*
|
||||
* @Inject({UrlShortener::class, VisitsTracker::class})
|
||||
*/
|
||||
public function __construct(UrlShortenerInterface $urlShortener, VisitsTrackerInterface $visitTracker)
|
||||
{
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->visitTracker = $visitTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming request and/or response.
|
||||
*
|
||||
* Accepts a server-side request and a response instance, and does
|
||||
* something with them.
|
||||
*
|
||||
* If the response is not complete and/or further processing would not
|
||||
* interfere with the work done in the middleware, or if the middleware
|
||||
* wants to delegate to another process, it can use the `$out` callable
|
||||
* if present.
|
||||
*
|
||||
* If the middleware does not return a value, execution of the current
|
||||
* request is considered complete, and the response instance provided will
|
||||
* be considered the response to return.
|
||||
*
|
||||
* Alternately, the middleware may return a response instance.
|
||||
*
|
||||
* Often, middleware will `return $out();`, with the assumption that a
|
||||
* later middleware will return a response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param null|callable $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode', '');
|
||||
|
||||
try {
|
||||
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
|
||||
// If provided shortCode does not belong to a valid long URL, dispatch next middleware, which is 404
|
||||
// middleware
|
||||
if (! isset($longUrl)) {
|
||||
return $out($request, $response);
|
||||
}
|
||||
|
||||
// Track visit to this shortcode
|
||||
$this->visitTracker->track($shortCode);
|
||||
|
||||
// Return a redirect response to the long URL.
|
||||
// Use a temporary redirect to make sure browsers always hit the server for analytics purposes
|
||||
return new RedirectResponse($longUrl);
|
||||
} catch (\Exception $e) {
|
||||
// In case of error, dispatch 404 error
|
||||
return $out($request, $response);
|
||||
}
|
||||
}
|
||||
}
|
154
src/Service/UrlShortener.php
Normal file
154
src/Service/UrlShortener.php
Normal file
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
|
||||
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
||||
use Acelaya\UrlShortener\Exception\RuntimeException;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
class UrlShortener implements UrlShortenerInterface
|
||||
{
|
||||
const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ';
|
||||
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $httpClient;
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $chars;
|
||||
|
||||
/**
|
||||
* UrlShortener constructor.
|
||||
* @param ClientInterface $httpClient
|
||||
* @param EntityManagerInterface $em
|
||||
* @param string $chars
|
||||
*
|
||||
* @Inject({"httpClient", "em", "config.url_shortener.shortcode_chars"})
|
||||
*/
|
||||
public function __construct(
|
||||
ClientInterface $httpClient,
|
||||
EntityManagerInterface $em,
|
||||
$chars = self::DEFAULT_CHARS
|
||||
) {
|
||||
$this->httpClient = $httpClient;
|
||||
$this->em = $em;
|
||||
$this->chars = $chars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and persists a unique shortcode generated for provided url
|
||||
*
|
||||
* @param UriInterface $url
|
||||
* @return string
|
||||
* @throws InvalidUrlException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function urlToShortCode(UriInterface $url)
|
||||
{
|
||||
// If the url already exists in the database, just return its short code
|
||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||
'originalUrl' => $url
|
||||
]);
|
||||
if (isset($shortUrl)) {
|
||||
return $shortUrl->getShortCode();
|
||||
}
|
||||
|
||||
// Check that the URL exists
|
||||
$this->checkUrlExists($url);
|
||||
|
||||
// Transactionally insert the short url, then generate the short code and finally update the short code
|
||||
try {
|
||||
$this->em->beginTransaction();
|
||||
|
||||
// First, create the short URL with an empty short code
|
||||
$shortUrl = new ShortUrl();
|
||||
$shortUrl->setOriginalUrl($url);
|
||||
$this->em->persist($shortUrl);
|
||||
$this->em->flush();
|
||||
|
||||
// Generate the short code and persist it
|
||||
$shortCode = $this->convertAutoincrementIdToShortCode($shortUrl->getId());
|
||||
$shortUrl->setShortCode($shortCode);
|
||||
$this->em->flush();
|
||||
|
||||
$this->em->commit();
|
||||
return $shortCode;
|
||||
} catch (ORMException $e) {
|
||||
if ($this->em->getConnection()->isTransactionActive()) {
|
||||
$this->em->rollback();
|
||||
$this->em->close();
|
||||
}
|
||||
|
||||
throw new RuntimeException('An error occured while persisting the short URL', -1, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to perform a GET request to provided url, returning true on success and false on failure
|
||||
*
|
||||
* @param UriInterface $url
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkUrlExists(UriInterface $url)
|
||||
{
|
||||
try {
|
||||
$this->httpClient->request('GET', $url);
|
||||
} catch (GuzzleException $e) {
|
||||
throw InvalidUrlException::fromUrl($url, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the unique shortcode for an autoincrement ID
|
||||
*
|
||||
* @param int $id
|
||||
* @return string
|
||||
*/
|
||||
protected function convertAutoincrementIdToShortCode($id)
|
||||
{
|
||||
$id = intval($id) + 200000; // Increment the Id so that the generated shortcode is not too short
|
||||
$length = strlen($this->chars);
|
||||
$code = '';
|
||||
|
||||
while ($id > 0) {
|
||||
// Determine the value of the next higher character in the short code and prepend it
|
||||
$code = $this->chars[intval(fmod($id, $length))] . $code;
|
||||
$id = floor($id / $length);
|
||||
}
|
||||
|
||||
return $this->chars[intval($id)] . $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the mapped URL for provided short code. Returns null if not found
|
||||
*
|
||||
* @param string $shortCode
|
||||
* @return string|null
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function shortCodeToUrl($shortCode)
|
||||
{
|
||||
// Validate short code format
|
||||
if (! preg_match('|[' . $this->chars . "]+|", $shortCode)) {
|
||||
throw InvalidShortCodeException::fromShortCode($shortCode, $this->chars);
|
||||
}
|
||||
|
||||
/** @var ShortUrl $shortUrl */
|
||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
return isset($shortUrl) ? $shortUrl->getOriginalUrl() : null;
|
||||
}
|
||||
}
|
29
src/Service/UrlShortenerInterface.php
Normal file
29
src/Service/UrlShortenerInterface.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
|
||||
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
||||
use Acelaya\UrlShortener\Exception\RuntimeException;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
interface UrlShortenerInterface
|
||||
{
|
||||
/**
|
||||
* Creates and persists a unique shortcode generated for provided url
|
||||
*
|
||||
* @param UriInterface $url
|
||||
* @return string
|
||||
* @throws InvalidUrlException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function urlToShortCode(UriInterface $url);
|
||||
|
||||
/**
|
||||
* Tries to find the mapped URL for provided short code. Returns null if not found
|
||||
*
|
||||
* @param string $shortCode
|
||||
* @return string|null
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function shortCodeToUrl($shortCode);
|
||||
}
|
61
src/Service/VisitsTracker.php
Normal file
61
src/Service/VisitsTracker.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Acelaya\UrlShortener\Entity\Visit;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class VisitsTracker implements VisitsTrackerInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
|
||||
/**
|
||||
* VisitsTracker constructor.
|
||||
* @param EntityManagerInterface $em
|
||||
*
|
||||
* @Inject({"em"})
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks a new visit to provided short code, using an array of data to look up information
|
||||
*
|
||||
* @param string $shortCode
|
||||
* @param array $visitorData Defaults to global $_SERVER
|
||||
*/
|
||||
public function track($shortCode, array $visitorData = null)
|
||||
{
|
||||
$visitorData = $visitorData ?: $_SERVER;
|
||||
|
||||
/** @var ShortUrl $shortUrl */
|
||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
|
||||
$visit = new Visit();
|
||||
$visit->setShortUrl($shortUrl)
|
||||
->setUserAgent($this->getArrayValue($visitorData, 'HTTP_USER_AGENT'))
|
||||
->setReferer($this->getArrayValue($visitorData, 'HTTP_REFERER'))
|
||||
->setRemoteAddr($this->getArrayValue($visitorData, 'REMOTE_ADDR'));
|
||||
$this->em->persist($visit);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array
|
||||
* @param $key
|
||||
* @param null $default
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function getArrayValue(array $array, $key, $default = null)
|
||||
{
|
||||
return isset($array[$key]) ? $array[$key] : $default;
|
||||
}
|
||||
}
|
13
src/Service/VisitsTrackerInterface.php
Normal file
13
src/Service/VisitsTrackerInterface.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
interface VisitsTrackerInterface
|
||||
{
|
||||
/**
|
||||
* Tracks a new visit to provided short code, using an array of data to look up information
|
||||
*
|
||||
* @param string $shortCode
|
||||
* @param array $visitorData Defaults to global $_SERVER
|
||||
*/
|
||||
public function track($shortCode, array $visitorData = null);
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
{% extends 'layout/default.html.twig' %}
|
||||
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<h1>Welcome to <span class="zf-green">zend-expressive</span></h1>
|
||||
<p>
|
||||
Congratulations! You have successfully installed the
|
||||
<a href="https://github.com/zendframework/zend-expressive-skeleton" target="_blank">zend-expressive skeleton application</a>.
|
||||
This skeleton can serve as a simple starting point for you to begin building your application.
|
||||
</p>
|
||||
<p>
|
||||
Expressive builds on zend-stratigility to provide a minimalist PSR-7 middleware framework for PHP.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h2>
|
||||
<a href="https://zendframework.github.io/zend-expressive/getting-started/features/" target="_blank">
|
||||
<i class="fa fa-refresh"></i> Agile & Lean
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
Expressive is fast, small and perfect for rapid application development, prototyping and api's. You decide how you
|
||||
extend it and choose the best packages from major framework or standalone projects.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h2>
|
||||
<a href="https://github.com/zendframework/zend-diactoros" target="_blank">
|
||||
<i class="fa fa-exchange"></i> HTTP Messages
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
HTTP messages are the foundation of web development. Web browsers and HTTP clients such as cURL create
|
||||
HTTP request messages that are sent to a web server, which provides an HTTP response message.
|
||||
Server-side code receives an HTTP request message, and returns an HTTP response message.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h2>
|
||||
<a href="https://github.com/zendframework/zend-stratigility" target="_blank">
|
||||
<i class="fa fa-dot-circle-o"></i> Middleware
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
Middleware is code that exists between the request and response, and which can take the incoming
|
||||
request, perform actions based on it, and either complete the response or pass delegation on to the
|
||||
next middleware in the queue. Your application is easily extended with custom middleware created by
|
||||
yourself or <a href="https://packagist.org/search/?q=middleware" target="_blank">others</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h2>
|
||||
<a href="https://zendframework.github.io/zend-expressive/features/container/intro/" target="_blank">
|
||||
<i class="fa fa-cube"></i> Containers
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
Expressive promotes and advocates the usage of Dependency Injection/Inversion of Control containers
|
||||
when writing your applications. Expressive supports multiple containers which typehints against
|
||||
<a href="https://github.com/container-interop/container-interop" target="_blank">container-interop</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h2>
|
||||
<a href="https://zendframework.github.io/zend-expressive/features/router/intro/" target="_blank">
|
||||
<i class="fa fa-plane"></i> Routers
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
One fundamental feature of zend-expressive is that it provides mechanisms for implementing dynamic
|
||||
routing, a feature required in most modern web applications. Expressive ships with multiple adapters.
|
||||
</p>
|
||||
{% if routerName is defined %}
|
||||
<p>
|
||||
<a href="{{ routerDocs }}" target="_blank">
|
||||
Get started with {{ routerName }}.
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h2>
|
||||
<a href="https://zendframework.github.io/zend-expressive/features/template/twig/" target="_blank">
|
||||
<i class="fa fa-files-o"></i> Templating
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
By default, no middleware in Expressive is templated. We do not even provide a default templating
|
||||
engine, as the choice of templating engine is often very specific to the project and/or organization.
|
||||
However, Expressive does provide abstraction for templating, which allows you to write middleware that
|
||||
is engine-agnostic.
|
||||
</p>
|
||||
{% if templateName is defined %}
|
||||
<p>
|
||||
<a href="{{ templateDocs }}" target="_blank">
|
||||
Get started with {{ templateName }}.
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,13 +1,17 @@
|
|||
{% extends 'layout/default.html.twig' %}
|
||||
|
||||
{% block title %}404 Not Found{% endblock %}
|
||||
{% block title %}URL Not Found{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
p {margin-bottom: 20px;}
|
||||
body {text-align: center;}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Oops!</h1>
|
||||
<h2>This is awkward.</h2>
|
||||
<p>We encountered a 404 Not Found error.</p>
|
||||
<p>
|
||||
You are looking for something that doesn't exist or may have moved. Check out one of the links on this page
|
||||
or head back to <a href="{{ path('home') }}">Home</a>.
|
||||
</p>
|
||||
<hr>
|
||||
<p>This short URL doesn't seem to be valid.</p>
|
||||
<p>Make sure you included all the characters, with no extra punctuation.</p>
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,14 +2,19 @@
|
|||
|
||||
{% block title %}{{ status }} {{ reason }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
p {margin-bottom: 20px;}
|
||||
body {text-align: center;}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Oops!</h1>
|
||||
<h2>This is awkward.</h2>
|
||||
<hr>
|
||||
<p>We encountered a {{ status }} {{ reason }} error.</p>
|
||||
{% if status == 404 %}
|
||||
<p>
|
||||
You are looking for something that doesn't exist or may have moved. Check out one of the links on this page
|
||||
or head back to <a href="{{ path('home') }}">Home</a>.
|
||||
</p>
|
||||
<p>This short URL doesn't seem to be valid.</p>
|
||||
<p>Make sure you included all the characters, with no extra punctuation.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,54 +3,19 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>{% block title %}{% endblock %} - zend-expressive</title>
|
||||
<title>{% block title %}{% endblock %} | URL shortener</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" />
|
||||
<style>
|
||||
body { padding-top: 60px; }
|
||||
.app { display: flex; min-height: 100vh; flex-direction: column; }
|
||||
.app-content { flex: 1; }
|
||||
.app-footer { padding-bottom: 1em; }
|
||||
.zf-green, h2 a { color: #68b604; }
|
||||
body {padding-top: 60px;}
|
||||
.app {display: flex; min-height: 100vh; flex-direction: column;}
|
||||
.app-content {flex: 1;}
|
||||
.app-footer p {margin-bottom: 20px;}
|
||||
</style>
|
||||
{% block stylesheets %}{% endblock %}
|
||||
</head>
|
||||
<body class="app">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{{ path('home') }}">
|
||||
<img src="{{ asset('zf-logo.png') }}" alt="Zend Expressive" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a href="https://zendframework.github.io/zend-expressive/" target="_blank">
|
||||
<i class="fa fa-book"></i> Docs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/zendframework/zend-expressive" target="_blank">
|
||||
<i class="fa fa-wrench"></i> Contribute
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="app-content">
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
|
@ -62,7 +27,7 @@
|
|||
<hr />
|
||||
{% block footer %}
|
||||
<p>
|
||||
© 2005 - {{ "now"|date("Y") }} by Zend Technologies Ltd. All rights reserved.
|
||||
© {{ "now" | date("Y") }} by <a href="http://www.alejandrocelaya.com">Alejandro Celaya</a>.
|
||||
</p>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
|
46
tests/Factory/CacheFactoryTest.php
Normal file
46
tests/Factory/CacheFactoryTest.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
namespace AcelayaTest\UrlShortener\Factory;
|
||||
|
||||
use Acelaya\UrlShortener\Factory\CacheFactory;
|
||||
use Doctrine\Common\Cache\ApcuCache;
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class CacheFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CacheFactory
|
||||
*/
|
||||
protected $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->factory = new CacheFactory();
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass()
|
||||
{
|
||||
putenv('APP_ENV');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function productionReturnsApcAdapter()
|
||||
{
|
||||
putenv('APP_ENV=pro');
|
||||
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||
$this->assertInstanceOf(ApcuCache::class, $instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function developmentReturnsArrayAdapter()
|
||||
{
|
||||
putenv('APP_ENV=dev');
|
||||
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||
$this->assertInstanceOf(ArrayCache::class, $instance);
|
||||
}
|
||||
}
|
38
tests/Factory/EntityManagerFactoryTest.php
Normal file
38
tests/Factory/EntityManagerFactoryTest.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
namespace AcelayaTest\UrlShortener\Factory;
|
||||
|
||||
use Acelaya\UrlShortener\Factory\EntityManagerFactory;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class EntityManagerFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerFactory
|
||||
*/
|
||||
protected $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->factory = new EntityManagerFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function serviceIsCreated()
|
||||
{
|
||||
$sm = new ServiceManager(['services' => [
|
||||
'config' => [
|
||||
'debug' => true,
|
||||
'database' => [
|
||||
'driver' => 'pdo_sqlite',
|
||||
],
|
||||
],
|
||||
]]);
|
||||
|
||||
$em = $this->factory->__invoke($sm, EntityManager::class);
|
||||
$this->assertInstanceOf(EntityManager::class, $em);
|
||||
}
|
||||
}
|
91
tests/Middleware/CliParamsMiddlewareTest.php
Normal file
91
tests/Middleware/CliParamsMiddlewareTest.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
namespace AcelayaTest\UrlShortener\Middleware;
|
||||
|
||||
use Acelaya\UrlShortener\Middleware\CliParamsMiddleware;
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
|
||||
class CliParamsMiddlewareTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function nonCliRequestsJustInvokeNextMiddleware()
|
||||
{
|
||||
$middleware = new CliParamsMiddleware([], 'non-cli');
|
||||
|
||||
$invoked = false;
|
||||
$originalResponse = new Response();
|
||||
|
||||
$response = $middleware->__invoke(
|
||||
ServerRequestFactory::fromGlobals(),
|
||||
$originalResponse,
|
||||
function ($req, $resp) use (&$invoked) {
|
||||
$invoked = true;
|
||||
return $resp;
|
||||
}
|
||||
);
|
||||
|
||||
$this->assertSame($originalResponse, $response);
|
||||
$this->assertTrue($invoked);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function nonSuccessRouteResultJustInvokesNextMiddleware()
|
||||
{
|
||||
$middleware = new CliParamsMiddleware([], 'cli');
|
||||
|
||||
$invoked = false;
|
||||
$originalResponse = new Response();
|
||||
$routeResult = $this->prophesize(RouteResult::class);
|
||||
$routeResult->isSuccess()->willReturn(false)->shouldBeCalledTimes(1);
|
||||
|
||||
$response = $middleware->__invoke(
|
||||
ServerRequestFactory::fromGlobals()->withAttribute(RouteResult::class, $routeResult->reveal()),
|
||||
$originalResponse,
|
||||
function ($req, $resp) use (&$invoked) {
|
||||
$invoked = true;
|
||||
return $resp;
|
||||
}
|
||||
);
|
||||
|
||||
$this->assertSame($originalResponse, $response);
|
||||
$this->assertTrue($invoked);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function properRouteWillInjectAttributeInResponse()
|
||||
{
|
||||
$expectedLongUrl = 'http://www.google.com';
|
||||
$middleware = new CliParamsMiddleware(['foo', 'bar', $expectedLongUrl], 'cli');
|
||||
|
||||
$invoked = false;
|
||||
$originalResponse = new Response();
|
||||
$routeResult = $this->prophesize(RouteResult::class);
|
||||
$routeResult->isSuccess()->willReturn(true)->shouldBeCalledTimes(1);
|
||||
$routeResult->getMatchedRouteName()->willReturn('cli-generate-shortcode')->shouldBeCalledTimes(1);
|
||||
/** @var ServerRequestInterface $request */
|
||||
$request = null;
|
||||
|
||||
$response = $middleware->__invoke(
|
||||
ServerRequestFactory::fromGlobals()->withAttribute(RouteResult::class, $routeResult->reveal()),
|
||||
$originalResponse,
|
||||
function ($req, $resp) use (&$invoked, &$request) {
|
||||
$invoked = true;
|
||||
$request = $req;
|
||||
return $resp;
|
||||
}
|
||||
);
|
||||
|
||||
$this->assertSame($originalResponse, $response);
|
||||
$this->assertEquals($expectedLongUrl, $request->getAttribute('longUrl'));
|
||||
$this->assertTrue($invoked);
|
||||
}
|
||||
}
|
29
tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php
Normal file
29
tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
namespace AcelayaTest\UrlShortener\Middleware\Factory;
|
||||
|
||||
use Acelaya\UrlShortener\Middleware\CliParamsMiddleware;
|
||||
use Acelaya\UrlShortener\Middleware\Factory\CliParamsMiddlewareFactory;
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class CliParamsMiddlewareFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CliParamsMiddlewareFactory
|
||||
*/
|
||||
protected $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->factory = new CliParamsMiddlewareFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function serviceIsCreated()
|
||||
{
|
||||
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||
$this->assertInstanceOf(CliParamsMiddleware::class, $instance);
|
||||
}
|
||||
}
|
136
tests/Service/UrlShortenerTest.php
Normal file
136
tests/Service/UrlShortenerTest.php
Normal file
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
namespace AcelayaTest\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Acelaya\UrlShortener\Service\UrlShortener;
|
||||
use Doctrine\Common\Persistence\ObjectRepository;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Zend\Diactoros\Uri;
|
||||
|
||||
class UrlShortenerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var UrlShortener
|
||||
*/
|
||||
protected $urlShortener;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $em;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->httpClient = $this->prophesize(ClientInterface::class);
|
||||
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$conn = $this->prophesize(Connection::class);
|
||||
$conn->isTransactionActive()->willReturn(false);
|
||||
$this->em->getConnection()->willReturn($conn->reveal());
|
||||
$this->em->flush()->willReturn(null);
|
||||
$this->em->commit()->willReturn(null);
|
||||
$this->em->beginTransaction()->willReturn(null);
|
||||
$this->em->persist(Argument::any())->will(function ($arguments) {
|
||||
/** @var ShortUrl $shortUrl */
|
||||
$shortUrl = $arguments[0];
|
||||
$shortUrl->setId(10);
|
||||
});
|
||||
$repo = $this->prophesize(ObjectRepository::class);
|
||||
$repo->findOneBy(Argument::any())->willReturn(null);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->urlShortener = new UrlShortener($this->httpClient->reveal(), $this->em->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function urlIsProperlyShortened()
|
||||
{
|
||||
// 10 -> 12C1c
|
||||
$shortCode = $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar'));
|
||||
$this->assertEquals('12C1c', $shortCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException \Acelaya\UrlShortener\Exception\RuntimeException
|
||||
*/
|
||||
public function exceptionIsThrownWhenOrmThrowsException()
|
||||
{
|
||||
$conn = $this->prophesize(Connection::class);
|
||||
$conn->isTransactionActive()->willReturn(true);
|
||||
$this->em->getConnection()->willReturn($conn->reveal());
|
||||
$this->em->rollback()->shouldBeCalledTimes(1);
|
||||
$this->em->close()->shouldBeCalledTimes(1);
|
||||
|
||||
$this->em->flush()->willThrow(new ORMException());
|
||||
$this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException \Acelaya\UrlShortener\Exception\InvalidUrlException
|
||||
*/
|
||||
public function exceptionIsThrownWhenUrlDoesNotExist()
|
||||
{
|
||||
$this->httpClient->request(Argument::cetera())->willThrow(
|
||||
new ClientException('', $this->prophesize(Request::class)->reveal())
|
||||
);
|
||||
$this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function whenShortUrlExistsItsShortcodeIsReturned()
|
||||
{
|
||||
$shortUrl = new ShortUrl();
|
||||
$shortUrl->setShortCode('expected_shortcode');
|
||||
$repo = $this->prophesize(ObjectRepository::class);
|
||||
$repo->findOneBy(Argument::any())->willReturn($shortUrl);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$shortCode = $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar'));
|
||||
$this->assertEquals($shortUrl->getShortCode(), $shortCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function shortCodeIsProperlyParsed()
|
||||
{
|
||||
// 12C1c -> 10
|
||||
$shortUrl = new ShortUrl();
|
||||
$shortUrl->setShortCode('12C1c')
|
||||
->setOriginalUrl('expected_url');
|
||||
|
||||
$repo = $this->prophesize(ObjectRepository::class);
|
||||
$repo->findOneBy(['shortCode' => '12C1c'])->willReturn($shortUrl);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$url = $this->urlShortener->shortCodeToUrl('12C1c');
|
||||
$this->assertEquals($shortUrl->getOriginalUrl(), $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException \Acelaya\UrlShortener\Exception\InvalidShortCodeException
|
||||
*/
|
||||
public function invalidCharSetThrowsException()
|
||||
{
|
||||
$this->urlShortener->shortCodeToUrl('&/(');
|
||||
}
|
||||
}
|
30
tests/Service/VisitsTrackerTest.php
Normal file
30
tests/Service/VisitsTrackerTest.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
namespace AcelayaTest\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Acelaya\UrlShortener\Service\VisitsTracker;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
|
||||
class VisitsTrackerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function trackPersistsVisit()
|
||||
{
|
||||
$shortCode = '123ABC';
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl());
|
||||
|
||||
$em = $this->prophesize(EntityManager::class);
|
||||
$em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
|
||||
$em->persist(Argument::any())->shouldBeCalledTimes(1);
|
||||
$em->flush()->shouldBeCalledTimes(1);
|
||||
|
||||
$visitsTracker = new VisitsTracker($em->reveal());
|
||||
$visitsTracker->track($shortCode);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue