Merge pull request #221 from acelaya/feature/chronos

Migrated from standard datetime objects to chronos objects
This commit is contained in:
Alejandro Celaya 2018-09-29 13:02:43 +02:00 committed by GitHub
commit ae9d99257e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 271 additions and 198 deletions

View File

@ -16,6 +16,7 @@
"ext-json": "*", "ext-json": "*",
"ext-pdo": "*", "ext-pdo": "*",
"acelaya/ze-content-based-error-handler": "^2.2", "acelaya/ze-content-based-error-handler": "^2.2",
"cakephp/chronos": "^1.2",
"cocur/slugify": "^3.0", "cocur/slugify": "^3.0",
"doctrine/cache": "^1.6", "doctrine/cache": "^1.6",
"doctrine/migrations": "^1.4", "doctrine/migrations": "^1.4",
@ -32,7 +33,7 @@
"theorchard/monolog-cascade": "^0.4", "theorchard/monolog-cascade": "^0.4",
"zendframework/zend-config": "^3.0", "zendframework/zend-config": "^3.0",
"zendframework/zend-config-aggregator": "^1.0", "zendframework/zend-config-aggregator": "^1.0",
"zendframework/zend-diactoros": "^1.7", "zendframework/zend-diactoros": "^2.0",
"zendframework/zend-expressive": "^3.0", "zendframework/zend-expressive": "^3.0",
"zendframework/zend-expressive-fastroute": "^3.0", "zendframework/zend-expressive-fastroute": "^3.0",
"zendframework/zend-expressive-helpers": "^5.0", "zendframework/zend-expressive-helpers": "^5.0",

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api; namespace Shlinkio\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -46,7 +47,7 @@ class GenerateKeyCommand extends Command
public function execute(InputInterface $input, OutputInterface $output) public function execute(InputInterface $input, OutputInterface $output)
{ {
$expirationDate = $input->getOption('expirationDate'); $expirationDate = $input->getOption('expirationDate');
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? new \DateTime($expirationDate) : null); $apiKey = $this->apiKeyService->create(isset($expirationDate) ? Chronos::parse($expirationDate) : null);
(new SymfonyStyle($input, $output))->success( (new SymfonyStyle($input, $output))->success(
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey) sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)

View File

@ -66,7 +66,7 @@ class ListKeysCommand extends Command
if (! $enabledOnly) { if (! $enabledOnly) {
$rowData[] = \sprintf($messagePattern, $this->getEnabledSymbol($row)); $rowData[] = \sprintf($messagePattern, $this->getEnabledSymbol($row));
} }
$rowData[] = $expiration !== null ? $expiration->format(\DateTime::ATOM) : '-'; $rowData[] = $expiration !== null ? $expiration->toAtomString() : '-';
$rows[] = $rowData; $rows[] = $rowData;
} }

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl; namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
@ -143,9 +144,9 @@ class GenerateShortUrlCommand extends Command
} }
} }
private function getOptionalDate(InputInterface $input, string $fieldName): ?\DateTime private function getOptionalDate(InputInterface $input, string $fieldName): ?Chronos
{ {
$since = $input->getOption($fieldName); $since = $input->getOption($fieldName);
return $since !== null ? new \DateTime($since) : null; return $since !== null ? Chronos::parse($since) : null;
} }
} }

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl; namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -107,10 +108,6 @@ class GetVisitsCommand extends Command
private function getDateOption(InputInterface $input, $key) private function getDateOption(InputInterface $input, $key)
{ {
$value = $input->getOption($key); $value = $input->getOption($key);
if (! empty($value)) { return ! empty($value) ? Chronos::parse($value) : $value;
$value = new \DateTime($value);
}
return $value;
} }
} }

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api; namespace ShlinkioTest\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@ -50,8 +51,8 @@ class GenerateKeyCommandTest extends TestCase
*/ */
public function expirationDateIsDefinedIfProvided() public function expirationDateIsDefinedIfProvided()
{ {
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1) $this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledTimes(1)
->willReturn(new ApiKey()); ->willReturn(new ApiKey());
$this->commandTester->execute([ $this->commandTester->execute([
'command' => 'api-key:generate', 'command' => 'api-key:generate',
'--expirationDate' => '2016-01-01', '--expirationDate' => '2016-01-01',

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl; namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@ -58,7 +59,7 @@ class GetVisitsCommandTest extends TestCase
$shortCode = 'abc123'; $shortCode = 'abc123';
$startDate = '2016-01-01'; $startDate = '2016-01-01';
$endDate = '2016-02-01'; $endDate = '2016-02-01';
$this->visitsTracker->info($shortCode, new DateRange(new \DateTime($startDate), new \DateTime($endDate))) $this->visitsTracker->info($shortCode, new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
->willReturn([]) ->willReturn([])
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);

View File

@ -5,10 +5,13 @@ namespace Shlinkio\Shlink\Common\Factory;
use Doctrine\Common\Cache\ArrayCache; use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache; use Doctrine\Common\Cache\Cache;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\Tools\Setup; use Doctrine\ORM\Tools\Setup;
use Interop\Container\ContainerInterface; use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException; use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
use Zend\ServiceManager\Exception\ServiceNotCreatedException; use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException; use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface; use Zend\ServiceManager\Factory\FactoryInterface;
@ -16,15 +19,10 @@ use Zend\ServiceManager\Factory\FactoryInterface;
class EntityManagerFactory implements 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 ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when creating a service. * @throws ServiceNotCreatedException if an exception is raised when creating a service.
* @throws ContainerException if any other error occurs * @throws ORMException
* @throws DBALException
*/ */
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{ {
@ -35,6 +33,8 @@ class EntityManagerFactory implements FactoryInterface
$connectionConfig = $emConfig['connection'] ?? []; $connectionConfig = $emConfig['connection'] ?? [];
$ormConfig = $emConfig['orm'] ?? []; $ormConfig = $emConfig['orm'] ?? [];
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
return EntityManager::create($connectionConfig, Setup::createAnnotationMetadataConfiguration( return EntityManager::create($connectionConfig, Setup::createAnnotationMetadataConfiguration(
$ormConfig['entities_paths'] ?? [], $ormConfig['entities_paths'] ?? [],
$isDevMode, $isDevMode,

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Type;
use Cake\Chronos\Chronos;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\DateTimeImmutableType;
class ChronosDateTimeType extends DateTimeImmutableType
{
public const CHRONOS_DATETIME = 'chronos_datetime';
public function getName(): string
{
return self::CHRONOS_DATETIME;
}
/**
* @throws ConversionException
*/
public function convertToPHPValue($value, AbstractPlatform $platform): ?Chronos
{
if ($value === null) {
return null;
}
$dateTime = parent::convertToPHPValue($value, $platform);
return Chronos::instance($dateTime);
}
/**
* @throws ConversionException
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if (null === $value) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format($platform->getDateTimeFormatString());
}
throw ConversionException::conversionFailedInvalidType(
$value,
$this->getName(),
['null', \DateTimeInterface::class]
);
}
}

View File

@ -3,42 +3,35 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util; namespace Shlinkio\Shlink\Common\Util;
class DateRange use Cake\Chronos\Chronos;
final class DateRange
{ {
/** /**
* @var \DateTimeInterface|null * @var Chronos|null
*/ */
private $startDate; private $startDate;
/** /**
* @var \DateTimeInterface|null * @var Chronos|null
*/ */
private $endDate; private $endDate;
public function __construct(\DateTimeInterface $startDate = null, \DateTimeInterface $endDate = null) public function __construct(?Chronos $startDate = null, ?Chronos $endDate = null)
{ {
$this->startDate = $startDate; $this->startDate = $startDate;
$this->endDate = $endDate; $this->endDate = $endDate;
} }
/** public function getStartDate(): ?Chronos
* @return \DateTimeInterface|null
*/
public function getStartDate()
{ {
return $this->startDate; return $this->startDate;
} }
/** public function getEndDate(): ?Chronos
* @return \DateTimeInterface|null
*/
public function getEndDate()
{ {
return $this->endDate; return $this->endDate;
} }
/**
* @return bool
*/
public function isEmpty(): bool public function isEmpty(): bool
{ {
return $this->startDate === null && $this->endDate === null; return $this->startDate === null && $this->endDate === null;

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Type;
use Cake\Chronos\Chronos;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
class ChronosDateTimeTypeTest extends TestCase
{
/**
* @var ChronosDateTimeType
*/
private $type;
public function setUp()
{
if (! Type::hasType(ChronosDateTimeType::CHRONOS_DATETIME)) {
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
}
$this->type = Type::getType(ChronosDateTimeType::CHRONOS_DATETIME);
}
/**
* @test
*/
public function nameIsReturned()
{
$this->assertEquals(ChronosDateTimeType::CHRONOS_DATETIME, $this->type->getName());
}
/**
* @test
* @dataProvider provideValues
*/
public function valueIsConverted(?string $value, ?string $expected)
{
$platform = $this->prophesize(AbstractPlatform::class);
$platform->getDateTimeFormatString()->willReturn('Y-m-d H:i:s');
$result = $this->type->convertToPHPValue($value, $platform->reveal());
if ($expected === null) {
$this->assertNull($result);
} else {
$this->assertInstanceOf($expected, $result);
}
}
public function provideValues(): array
{
return [
[null, null],
['now', Chronos::class],
['2017-01-01', Chronos::class],
];
}
/**
* @test
* @dataProvider providePhpValues
*/
public function valueIsConvertedToDatabaseFormat(?\DateTimeInterface $value, ?string $expected)
{
$platform = $this->prophesize(AbstractPlatform::class);
$platform->getDateTimeFormatString()->willReturn('Y-m-d');
$this->assertEquals($expected, $this->type->convertToDatabaseValue($value, $platform->reveal()));
}
public function providePhpValues(): array
{
return [
[null, null],
[new \DateTimeImmutable('2017-01-01'), '2017-01-01'],
[Chronos::parse('2017-02-01'), '2017-02-01'],
[new \DateTime('2017-03-01'), '2017-03-01'],
];
}
/**
* @test
*/
public function exceptionIsThrownIfInvalidValueIsParsedToDatabase()
{
$this->expectException(ConversionException::class);
$this->type->convertToDatabaseValue(new \stdClass(), $this->prophesize(AbstractPlatform::class)->reveal());
}
}

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Util; namespace ShlinkioTest\Shlink\Common\Util;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
@ -24,8 +25,8 @@ class DateRangeTest extends TestCase
*/ */
public function providedDatesAreSet() public function providedDatesAreSet()
{ {
$startDate = new \DateTime(); $startDate = Chronos::now();
$endDate = new \DateTime(); $endDate = Chronos::now();
$range = new DateRange($startDate, $endDate); $range = new DateRange($startDate, $endDate);
$this->assertSame($startDate, $range->getStartDate()); $this->assertSame($startDate, $range->getStartDate());
$this->assertSame($endDate, $range->getEndDate()); $this->assertSame($endDate, $range->getEndDate());

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Entity; namespace Shlinkio\Shlink\Core\Entity;
use Cake\Chronos\Chronos;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
@ -36,8 +37,8 @@ class ShortUrl extends AbstractEntity
*/ */
private $shortCode; private $shortCode;
/** /**
* @var \DateTime * @var Chronos
* @ORM\Column(name="date_created", type="datetime") * @ORM\Column(name="date_created", type="chronos_datetime")
*/ */
private $dateCreated; private $dateCreated;
/** /**
@ -56,13 +57,13 @@ class ShortUrl extends AbstractEntity
*/ */
private $tags; private $tags;
/** /**
* @var \DateTime * @var Chronos|null
* @ORM\Column(name="valid_since", type="datetime", nullable=true) * @ORM\Column(name="valid_since", type="chronos_datetime", nullable=true)
*/ */
private $validSince; private $validSince;
/** /**
* @var \DateTime * @var Chronos|null
* @ORM\Column(name="valid_until", type="datetime", nullable=true) * @ORM\Column(name="valid_until", type="chronos_datetime", nullable=true)
*/ */
private $validUntil; private $validUntil;
/** /**
@ -74,7 +75,7 @@ class ShortUrl extends AbstractEntity
public function __construct() public function __construct()
{ {
$this->shortCode = ''; $this->shortCode = '';
$this->dateCreated = new \DateTime(); $this->dateCreated = Chronos::now();
$this->visits = new ArrayCollection(); $this->visits = new ArrayCollection();
$this->tags = new ArrayCollection(); $this->tags = new ArrayCollection();
} }
@ -117,12 +118,12 @@ class ShortUrl extends AbstractEntity
return $this; return $this;
} }
public function getDateCreated(): \DateTime public function getDateCreated(): Chronos
{ {
return $this->dateCreated; return $this->dateCreated;
} }
public function setDateCreated(\DateTime $dateCreated): self public function setDateCreated(Chronos $dateCreated): self
{ {
$this->dateCreated = $dateCreated; $this->dateCreated = $dateCreated;
return $this; return $this;
@ -151,23 +152,23 @@ class ShortUrl extends AbstractEntity
return $this; return $this;
} }
public function getValidSince(): ?\DateTime public function getValidSince(): ?Chronos
{ {
return $this->validSince; return $this->validSince;
} }
public function setValidSince(?\DateTime $validSince): self public function setValidSince(?Chronos $validSince): self
{ {
$this->validSince = $validSince; $this->validSince = $validSince;
return $this; return $this;
} }
public function getValidUntil(): ?\DateTime public function getValidUntil(): ?Chronos
{ {
return $this->validUntil; return $this->validUntil;
} }
public function setValidUntil(?\DateTime $validUntil): self public function setValidUntil(?Chronos $validUntil): self
{ {
$this->validUntil = $validUntil; $this->validUntil = $validUntil;
return $this; return $this;

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Entity; namespace Shlinkio\Shlink\Core\Entity;
use Cake\Chronos\Chronos;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Shlinkio\Shlink\Common\Entity\AbstractEntity; use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Common\Exception\WrongIpException; use Shlinkio\Shlink\Common\Exception\WrongIpException;
@ -25,8 +26,8 @@ class Visit extends AbstractEntity implements \JsonSerializable
*/ */
private $referer; private $referer;
/** /**
* @var \DateTime * @var Chronos
* @ORM\Column(type="datetime", nullable=false) * @ORM\Column(type="chronos_datetime", nullable=false)
*/ */
private $date; private $date;
/** /**
@ -54,7 +55,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
public function __construct() public function __construct()
{ {
$this->date = new \DateTime(); $this->date = Chronos::now();
} }
public function getReferer(): string public function getReferer(): string
@ -68,12 +69,12 @@ class Visit extends AbstractEntity implements \JsonSerializable
return $this; return $this;
} }
public function getDate(): \DateTime public function getDate(): Chronos
{ {
return $this->date; return $this->date;
} }
public function setDate(\DateTime $date): self public function setDate(Chronos $date): self
{ {
$this->date = $date; $this->date = $date;
return $this; return $this;
@ -148,7 +149,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
{ {
return [ return [
'referer' => $this->referer, 'referer' => $this->referer,
'date' => isset($this->date) ? $this->date->format(\DateTime::ATOM) : null, 'date' => isset($this->date) ? $this->date->toAtomString() : null,
'userAgent' => $this->userAgent, 'userAgent' => $this->userAgent,
'visitLocation' => $this->visitLocation, 'visitLocation' => $this->visitLocation,

View File

@ -3,17 +3,19 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model; namespace Shlinkio\Shlink\Core\Model;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter; use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
use function is_string;
final class ShortUrlMeta final class ShortUrlMeta
{ {
/** /**
* @var \DateTime|null * @var Chronos|null
*/ */
private $validSince; private $validSince;
/** /**
* @var \DateTime|null * @var Chronos|null
*/ */
private $validUntil; private $validUntil;
/** /**
@ -43,8 +45,8 @@ final class ShortUrlMeta
} }
/** /**
* @param string|\DateTimeInterface|null $validSince * @param string|Chronos|null $validSince
* @param string|\DateTimeInterface|null $validUntil * @param string|Chronos|null $validUntil
* @param string|null $customSlug * @param string|null $customSlug
* @param int|null $maxVisits * @param int|null $maxVisits
* @return ShortUrlMeta * @return ShortUrlMeta
@ -86,26 +88,23 @@ final class ShortUrlMeta
} }
/** /**
* @param string|\DateTime|null $date * @param string|Chronos|null $date
* @return \DateTime|null * @return Chronos|null
*/ */
private function parseDateField($date): ?\DateTime private function parseDateField($date): ?Chronos
{ {
if ($date === null || $date instanceof \DateTime) { if ($date === null || $date instanceof Chronos) {
return $date; return $date;
} }
if (\is_string($date)) { if (is_string($date)) {
return new \DateTime($date); return Chronos::parse($date);
} }
return null; return null;
} }
/** public function getValidSince(): ?Chronos
* @return \DateTime|null
*/
public function getValidSince(): ?\DateTime
{ {
return $this->validSince; return $this->validSince;
} }
@ -115,10 +114,7 @@ final class ShortUrlMeta
return $this->validSince !== null; return $this->validSince !== null;
} }
/** public function getValidUntil(): ?Chronos
* @return \DateTime|null
*/
public function getValidUntil(): ?\DateTime
{ {
return $this->validUntil; return $this->validUntil;
} }

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository; namespace Shlinkio\Shlink\Core\Repository;
use Cake\Chronos\Chronos;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
@ -131,7 +132,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
*/ */
public function findOneByShortCode(string $shortCode): ?ShortUrl public function findOneByShortCode(string $shortCode): ?ShortUrl
{ {
$now = new \DateTimeImmutable(); $now = Chronos::now();
$qb = $this->createQueryBuilder('s'); $qb = $this->createQueryBuilder('s');
$qb->where($qb->expr()->eq('s.shortCode', ':shortCode')) $qb->where($qb->expr()->eq('s.shortCode', ':shortCode'))

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Service; namespace Shlinkio\Shlink\Core\Service;
use Cake\Chronos\Chronos;
use Cocur\Slugify\Slugify; use Cocur\Slugify\Slugify;
use Cocur\Slugify\SlugifyInterface; use Cocur\Slugify\SlugifyInterface;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -61,14 +62,6 @@ class UrlShortener implements UrlShortenerInterface
} }
/** /**
* Creates and persists a unique shortcode generated for provided url
*
* @param UriInterface $url
* @param string[] $tags
* @param \DateTime|null $validSince
* @param \DateTime|null $validUntil
* @param string|null $customSlug
* @param int|null $maxVisits
* @throws NonUniqueSlugException * @throws NonUniqueSlugException
* @throws InvalidUrlException * @throws InvalidUrlException
* @throws RuntimeException * @throws RuntimeException
@ -76,10 +69,10 @@ class UrlShortener implements UrlShortenerInterface
public function urlToShortCode( public function urlToShortCode(
UriInterface $url, UriInterface $url,
array $tags = [], array $tags = [],
\DateTime $validSince = null, ?Chronos $validSince = null,
\DateTime $validUntil = null, ?Chronos $validUntil = null,
string $customSlug = null, ?string $customSlug = null,
int $maxVisits = null ?int $maxVisits = null
): ShortUrl { ): ShortUrl {
// If the URL validation is enabled, check that the URL actually exists // If the URL validation is enabled, check that the URL actually exists
if ($this->urlValidationEnabled) { if ($this->urlValidationEnabled) {

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Service; namespace Shlinkio\Shlink\Core\Service;
use Cake\Chronos\Chronos;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
@ -14,14 +15,6 @@ use Shlinkio\Shlink\Core\Exception\RuntimeException;
interface UrlShortenerInterface interface UrlShortenerInterface
{ {
/** /**
* Creates and persists a unique shortcode generated for provided url
*
* @param UriInterface $url
* @param string[] $tags
* @param \DateTime|null $validSince
* @param \DateTime|null $validUntil
* @param string|null $customSlug
* @param int|null $maxVisits
* @throws NonUniqueSlugException * @throws NonUniqueSlugException
* @throws InvalidUrlException * @throws InvalidUrlException
* @throws RuntimeException * @throws RuntimeException
@ -29,10 +22,10 @@ interface UrlShortenerInterface
public function urlToShortCode( public function urlToShortCode(
UriInterface $url, UriInterface $url,
array $tags = [], array $tags = [],
\DateTime $validSince = null, ?Chronos $validSince = null,
\DateTime $validUntil = null, ?Chronos $validUntil = null,
string $customSlug = null, ?string $customSlug = null,
int $maxVisits = null ?int $maxVisits = null
): ShortUrl; ): ShortUrl;
/** /**

View File

@ -24,7 +24,6 @@ class ShortUrlDataTransformer implements DataTransformerInterface
/** /**
* @param ShortUrl $value * @param ShortUrl $value
* @return array
*/ */
public function transform($value): array public function transform($value): array
{ {
@ -36,7 +35,7 @@ class ShortUrlDataTransformer implements DataTransformerInterface
'shortCode' => $shortCode, 'shortCode' => $shortCode,
'shortUrl' => $this->buildShortUrl($this->domainConfig, $shortCode), 'shortUrl' => $this->buildShortUrl($this->domainConfig, $shortCode),
'longUrl' => $longUrl, 'longUrl' => $longUrl,
'dateCreated' => $dateCreated !== null ? $dateCreated->format(\DateTime::ATOM) : null, 'dateCreated' => $dateCreated !== null ? $dateCreated->toAtomString() : null,
'visitsCount' => $value->getVisitsCount(), 'visitsCount' => $value->getVisitsCount(),
'tags' => \array_map([$this, 'serializeTag'], $value->getTags()->toArray()), 'tags' => \array_map([$this, 'serializeTag'], $value->getTags()->toArray()),

View File

@ -12,12 +12,12 @@ class ShortUrlMetaInputFilter extends InputFilter
{ {
use InputFactoryTrait; use InputFactoryTrait;
const VALID_SINCE = 'validSince'; public const VALID_SINCE = 'validSince';
const VALID_UNTIL = 'validUntil'; public const VALID_UNTIL = 'validUntil';
const CUSTOM_SLUG = 'customSlug'; public const CUSTOM_SLUG = 'customSlug';
const MAX_VISITS = 'maxVisits'; public const MAX_VISITS = 'maxVisits';
public function __construct(array $data = null) public function __construct(?array $data = null)
{ {
$this->initialize(); $this->initialize();
if ($data !== null) { if ($data !== null) {
@ -25,7 +25,7 @@ class ShortUrlMetaInputFilter extends InputFilter
} }
} }
private function initialize() private function initialize(): void
{ {
$validSince = $this->createInput(self::VALID_SINCE, false); $validSince = $this->createInput(self::VALID_SINCE, false);
$validSince->getValidatorChain()->attach(new Date(['format' => \DateTime::ATOM])); $validSince->getValidatorChain()->attach(new Date(['format' => \DateTime::ATOM]));

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Repository; namespace ShlinkioTest\Shlink\Core\Repository;
use Cake\Chronos\Chronos;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Entity\Tag;
@ -41,7 +42,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$bar = new ShortUrl(); $bar = new ShortUrl();
$bar->setOriginalUrl('bar') $bar->setOriginalUrl('bar')
->setShortCode('bar_very_long_text') ->setShortCode('bar_very_long_text')
->setValidSince((new \DateTime())->add(new \DateInterval('P1M'))); ->setValidSince(Chronos::now()->addMonth());
$this->getEntityManager()->persist($bar); $this->getEntityManager()->persist($bar);
$visits = []; $visits = [];

View File

@ -3,12 +3,14 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Repository; namespace ShlinkioTest\Shlink\Core\Repository;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Repository\VisitRepository;
use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase; use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase;
use function sprintf;
class VisitRepositoryTest extends DatabaseTestCase class VisitRepositoryTest extends DatabaseTestCase
{ {
@ -61,7 +63,7 @@ class VisitRepositoryTest extends DatabaseTestCase
for ($i = 0; $i < 6; $i++) { for ($i = 0; $i < 6; $i++) {
$visit = new Visit(); $visit = new Visit();
$visit->setShortUrl($shortUrl) $visit->setShortUrl($shortUrl)
->setDate(new \DateTime('2016-01-0' . ($i + 1))); ->setDate(Chronos::parse(sprintf('2016-01-0%s', $i + 1)));
$this->getEntityManager()->persist($visit); $this->getEntityManager()->persist($visit);
} }
@ -70,11 +72,11 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->assertCount(0, $this->repo->findVisitsByShortUrl('invalid')); $this->assertCount(0, $this->repo->findVisitsByShortUrl('invalid'));
$this->assertCount(6, $this->repo->findVisitsByShortUrl($shortUrl->getId())); $this->assertCount(6, $this->repo->findVisitsByShortUrl($shortUrl->getId()));
$this->assertCount(2, $this->repo->findVisitsByShortUrl($shortUrl->getId(), new DateRange( $this->assertCount(2, $this->repo->findVisitsByShortUrl($shortUrl->getId(), new DateRange(
new \DateTime('2016-01-02'), Chronos::parse('2016-01-02'),
new \DateTime('2016-01-03') Chronos::parse('2016-01-03')
))); )));
$this->assertCount(4, $this->repo->findVisitsByShortUrl($shortUrl->getId(), new DateRange( $this->assertCount(4, $this->repo->findVisitsByShortUrl($shortUrl->getId(), new DateRange(
new \DateTime('2016-01-03') Chronos::parse('2016-01-03')
))); )));
} }
} }

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Model; namespace ShlinkioTest\Shlink\Core\Model;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
@ -42,10 +43,10 @@ class ShortUrlMetaTest extends TestCase
*/ */
public function properlyCreatedInstanceReturnsValues() public function properlyCreatedInstanceReturnsValues()
{ {
$meta = ShortUrlMeta::createFromParams((new \DateTime('2015-01-01'))->format(\DateTime::ATOM), null, 'foobar'); $meta = ShortUrlMeta::createFromParams(Chronos::parse('2015-01-01')->toAtomString(), null, 'foobar');
$this->assertTrue($meta->hasValidSince()); $this->assertTrue($meta->hasValidSince());
$this->assertEquals(new \DateTime('2015-01-01'), $meta->getValidSince()); $this->assertEquals(Chronos::parse('2015-01-01'), $meta->getValidSince());
$this->assertFalse($meta->hasValidUntil()); $this->assertFalse($meta->hasValidUntil());
$this->assertNull($meta->getValidUntil()); $this->assertNull($meta->getValidUntil());

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Service; namespace ShlinkioTest\Shlink\Core\Service;
use Cake\Chronos\Chronos;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -104,15 +105,15 @@ class ShortUrlServiceTest extends TestCase
$flush = $this->em->flush($shortUrl)->willReturn(null); $flush = $this->em->flush($shortUrl)->willReturn(null);
$result = $this->service->updateMetadataByShortCode('abc123', ShortUrlMeta::createFromParams( $result = $this->service->updateMetadataByShortCode('abc123', ShortUrlMeta::createFromParams(
(new \DateTime('2017-01-01 00:00:00'))->format(\DateTime::ATOM), Chronos::parse('2017-01-01 00:00:00')->toAtomString(),
(new \DateTime('2017-01-05 00:00:00'))->format(\DateTime::ATOM), Chronos::parse('2017-01-05 00:00:00')->toAtomString(),
null, null,
5 5
)); ));
$this->assertSame($shortUrl, $result); $this->assertSame($shortUrl, $result);
$this->assertEquals(new \DateTime('2017-01-01 00:00:00'), $shortUrl->getValidSince()); $this->assertEquals(Chronos::parse('2017-01-01 00:00:00'), $shortUrl->getValidSince());
$this->assertEquals(new \DateTime('2017-01-05 00:00:00'), $shortUrl->getValidUntil()); $this->assertEquals(Chronos::parse('2017-01-05 00:00:00'), $shortUrl->getValidUntil());
$this->assertEquals(5, $shortUrl->getMaxVisits()); $this->assertEquals(5, $shortUrl->getMaxVisits());
$findShortUrl->shouldHaveBeenCalled(); $findShortUrl->shouldHaveBeenCalled();
$getRepo->shouldHaveBeenCalled(); $getRepo->shouldHaveBeenCalled();

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\ShortUrl; namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
use Cake\Chronos\Chronos;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\CreateShortUrlData; use Shlinkio\Shlink\Core\Model\CreateShortUrlData;
@ -40,8 +41,8 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction
); );
} }
private function getOptionalDate(array $postData, string $fieldName) private function getOptionalDate(array $postData, string $fieldName): ?Chronos
{ {
return isset($postData[$fieldName]) ? new \DateTime($postData[$fieldName]) : null; return isset($postData[$fieldName]) ? Chronos::parse($postData[$fieldName]) : null;
} }
} }

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Visit; namespace Shlinkio\Shlink\Rest\Action\Visit;
use Cake\Chronos\Chronos;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -75,18 +76,9 @@ class GetVisitsAction extends AbstractRestAction
} }
} }
/** private function getDateQueryParam(Request $request, string $key): ?Chronos
* @param Request $request
* @param string $key
* @return \DateTime|null
*/
private function getDateQueryParam(Request $request, string $key)
{ {
$query = $request->getQueryParams(); $query = $request->getQueryParams();
if (! isset($query[$key]) || empty($query[$key])) { return ! isset($query[$key]) || empty($query[$key]) ? null : Chronos::parse($query[$key]);
return null;
}
return new \DateTime($query[$key]);
} }
} }

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Entity; namespace Shlinkio\Shlink\Rest\Entity;
use Cake\Chronos\Chronos;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Shlinkio\Shlink\Common\Entity\AbstractEntity; use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait; use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
@ -25,8 +26,8 @@ class ApiKey extends AbstractEntity
*/ */
private $key; private $key;
/** /**
* @var \DateTime|null * @var Chronos|null
* @ORM\Column(name="expiration_date", nullable=true, type="datetime") * @ORM\Column(name="expiration_date", nullable=true, type="chronos_datetime")
*/ */
private $expirationDate; private $expirationDate;
/** /**
@ -52,12 +53,12 @@ class ApiKey extends AbstractEntity
return $this; return $this;
} }
public function getExpirationDate(): ?\DateTime public function getExpirationDate(): ?Chronos
{ {
return $this->expirationDate; return $this->expirationDate;
} }
public function setExpirationDate(\DateTime $expirationDate): self public function setExpirationDate(Chronos $expirationDate): self
{ {
$this->expirationDate = $expirationDate; $this->expirationDate = $expirationDate;
return $this; return $this;
@ -69,7 +70,7 @@ class ApiKey extends AbstractEntity
return false; return false;
} }
return $this->expirationDate < new \DateTime(); return $this->expirationDate->lt(Chronos::now());
} }
public function isEnabled(): bool public function isEnabled(): bool

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Service; namespace Shlinkio\Shlink\Rest\Service;
use Cake\Chronos\Chronos;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
@ -20,13 +21,7 @@ class ApiKeyService implements ApiKeyServiceInterface
$this->em = $em; $this->em = $em;
} }
/** public function create(?Chronos $expirationDate = null): ApiKey
* Creates a new ApiKey with provided expiration date
*
* @param \DateTime $expirationDate
* @return ApiKey
*/
public function create(\DateTime $expirationDate = null): ApiKey
{ {
$key = new ApiKey(); $key = new ApiKey();
if ($expirationDate !== null) { if ($expirationDate !== null) {
@ -39,12 +34,6 @@ class ApiKeyService implements ApiKeyServiceInterface
return $key; return $key;
} }
/**
* Checks if provided key is a valid api key
*
* @param string $key
* @return bool
*/
public function check(string $key): bool public function check(string $key): bool
{ {
/** @var ApiKey|null $apiKey */ /** @var ApiKey|null $apiKey */
@ -53,10 +42,6 @@ class ApiKeyService implements ApiKeyServiceInterface
} }
/** /**
* Disables provided api key
*
* @param string $key
* @return ApiKey
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function disable(string $key): ApiKey public function disable(string $key): ApiKey
@ -72,12 +57,6 @@ class ApiKeyService implements ApiKeyServiceInterface
return $apiKey; return $apiKey;
} }
/**
* Lists all existing api keys
*
* @param bool $enabledOnly Tells if only enabled keys should be returned
* @return ApiKey[]
*/
public function listKeys(bool $enabledOnly = false): array public function listKeys(bool $enabledOnly = false): array
{ {
$conditions = $enabledOnly ? ['enabled' => true] : []; $conditions = $enabledOnly ? ['enabled' => true] : [];
@ -86,12 +65,6 @@ class ApiKeyService implements ApiKeyServiceInterface
return $apiKeys; return $apiKeys;
} }
/**
* Tries to find one API key by its key string
*
* @param string $key
* @return ApiKey|null
*/
public function getByKey(string $key): ?ApiKey public function getByKey(string $key): ?ApiKey
{ {
/** @var ApiKey|null $apiKey */ /** @var ApiKey|null $apiKey */

View File

@ -3,49 +3,22 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Service; namespace Shlinkio\Shlink\Rest\Service;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
interface ApiKeyServiceInterface interface ApiKeyServiceInterface
{ {
/** public function create(?Chronos $expirationDate = null): ApiKey;
* Creates a new ApiKey with provided expiration date
*
* @param \DateTime $expirationDate
* @return ApiKey
*/
public function create(\DateTime $expirationDate = null): ApiKey;
/**
* Checks if provided key is a valid api key
*
* @param string $key
* @return bool
*/
public function check(string $key): bool; public function check(string $key): bool;
/** /**
* Disables provided api key
*
* @param string $key
* @return ApiKey
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function disable(string $key): ApiKey; public function disable(string $key): ApiKey;
/**
* Lists all existing api keys
*
* @param bool $enabledOnly Tells if only enabled keys should be returned
* @return ApiKey[]
*/
public function listKeys(bool $enabledOnly = false): array; public function listKeys(bool $enabledOnly = false): array;
/**
* Tries to find one API key by its key string
*
* @param string $key
* @return ApiKey|null
*/
public function getByKey(string $key): ?ApiKey; public function getByKey(string $key): ?ApiKey;
} }

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Visit; namespace ShlinkioTest\Shlink\Rest\Action\Visit;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@ -77,7 +78,7 @@ class GetVisitsActionTest extends TestCase
public function datesAreReadFromQuery() public function datesAreReadFromQuery()
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->visitsTracker->info($shortCode, new DateRange(null, new \DateTime('2016-01-01 00:00:00'))) $this->visitsTracker->info($shortCode, new DateRange(null, Chronos::parse('2016-01-01 00:00:00')))
->willReturn([]) ->willReturn([])
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);

View File

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Service; namespace ShlinkioTest\Shlink\Rest\Service;
use Cake\Chronos\Chronos;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -48,7 +49,7 @@ class ApiKeyServiceTest extends TestCase
$this->em->flush()->shouldBeCalledTimes(1); $this->em->flush()->shouldBeCalledTimes(1);
$this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledTimes(1); $this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledTimes(1);
$date = new \DateTime('2030-01-01'); $date = Chronos::parse('2030-01-01');
$key = $this->service->create($date); $key = $this->service->create($date);
$this->assertSame($date, $key->getExpirationDate()); $this->assertSame($date, $key->getExpirationDate());
} }
@ -87,7 +88,7 @@ class ApiKeyServiceTest extends TestCase
public function checkReturnsFalseWhenKeyIsExpired() public function checkReturnsFalseWhenKeyIsExpired()
{ {
$key = new ApiKey(); $key = new ApiKey();
$key->setExpirationDate((new \DateTime())->sub(new \DateInterval('P1D'))); $key->setExpirationDate(Chronos::now()->subDay());
$repo = $this->prophesize(EntityRepository::class); $repo = $this->prophesize(EntityRepository::class);
$repo->findOneBy(['key' => '12345'])->willReturn($key) $repo->findOneBy(['key' => '12345'])->willReturn($key)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);