mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 23:33:13 +08:00
Created Core module
This commit is contained in:
43
module/Core/src/Service/ShortUrlService.php
Normal file
43
module/Core/src/Service/ShortUrlService.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class ShortUrlService implements ShortUrlServiceInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
|
||||
/**
|
||||
* ShortUrlService constructor.
|
||||
* @param EntityManagerInterface $em
|
||||
*
|
||||
* @Inject({"em"})
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
* @return Paginator|ShortUrl[]
|
||||
*/
|
||||
public function listShortUrls($page = 1)
|
||||
{
|
||||
/** @var ShortUrlRepository $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
$paginator = new Paginator(new PaginableRepositoryAdapter($repo));
|
||||
$paginator->setItemCountPerPage(PaginableRepositoryAdapter::ITEMS_PER_PAGE)
|
||||
->setCurrentPageNumber($page);
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
}
|
||||
14
module/Core/src/Service/ShortUrlServiceInterface.php
Normal file
14
module/Core/src/Service/ShortUrlServiceInterface.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
interface ShortUrlServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $page
|
||||
* @return ShortUrl[]|Paginator
|
||||
*/
|
||||
public function listShortUrls($page = 1);
|
||||
}
|
||||
154
module/Core/src/Service/UrlShortener.php
Normal file
154
module/Core/src/Service/UrlShortener.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
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;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||
|
||||
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 = empty($chars) ? self::DEFAULT_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
module/Core/src/Service/UrlShortenerInterface.php
Normal file
29
module/Core/src/Service/UrlShortenerInterface.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||
|
||||
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);
|
||||
}
|
||||
86
module/Core/src/Service/VisitsTracker.php
Normal file
86
module/Core/src/Service/VisitsTracker.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visits on certain shortcode
|
||||
*
|
||||
* @param $shortCode
|
||||
* @return Paginator|Visit[]
|
||||
*/
|
||||
public function info($shortCode)
|
||||
{
|
||||
/** @var ShortUrl $shortUrl */
|
||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
if (! isset($shortUrl)) {
|
||||
throw new InvalidArgumentException(sprintf('Short code "%s" not found', $shortCode));
|
||||
}
|
||||
|
||||
return $this->em->getRepository(Visit::class)->findBy([
|
||||
'shortUrl' => $shortUrl,
|
||||
], [
|
||||
'date' => 'DESC'
|
||||
]);
|
||||
}
|
||||
}
|
||||
24
module/Core/src/Service/VisitsTrackerInterface.php
Normal file
24
module/Core/src/Service/VisitsTrackerInterface.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
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);
|
||||
|
||||
/**
|
||||
* Returns the visits on certain shortcode
|
||||
*
|
||||
* @param $shortCode
|
||||
* @return Paginator|Visit[]
|
||||
*/
|
||||
public function info($shortCode);
|
||||
}
|
||||
Reference in New Issue
Block a user