diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a1d7ca..19213b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ CHANGELOG for 1.x =================== +## v1.2.0 - (2024-03-27) +### Added +- Common Entity Interface and Trait such as the `ProcessInterface` which we will use to monitor cron, api and file generation. +- `ProcessMonitor` to centralize process code management +- `CommandPoolHelper` service to fetch data about the project symfony commands (like getting all cron choices) +- `DateUtils::secondsToString` helper to convert seconds into a small summary string +- `IniOverrideConfig::initDefaultTimezoneForCli` helper to properly set the timezone when using date with PHP CLI on CleverCloud +- `ApiCallInterface` and trait to ease monitoring API calls + +### Fixed +- `RegexUtils::PHONE_PATTERN` remove wrong extra digit needed on foreign number + ## v1.1.0 - (2024-03-25) ### Added - new `ArrayUtils` methods : `checkIssetKeys`, `trimExplode`, `removeEmpty`, `filterByPattern`, `flatToMap` diff --git a/config/services.yaml b/config/services.yaml index b82fbb6..e4a05cc 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,4 +1,9 @@ services: + # Command + Smart\CoreBundle\Command\CommandPoolHelper: + arguments: + - '@Symfony\Component\HttpKernel\KernelInterface' + - '@translator' # Config Smart\CoreBundle\Config\IniOverrideConfig: arguments: @@ -9,6 +14,10 @@ services: - setContainer: [ '@service_container' ] - setEntityManager: [ '@Doctrine\ORM\EntityManagerInterface' ] tags: [ 'controller.service_arguments' ] + # Monitoring + Smart\CoreBundle\Monitoring\ProcessMonitor: + arguments: + - '@Doctrine\ORM\EntityManagerInterface' # Route Smart\CoreBundle\Route\RouteLoader: tags: diff --git a/src/Command/CommandPoolHelper.php b/src/Command/CommandPoolHelper.php new file mode 100644 index 0000000..e788713 --- /dev/null +++ b/src/Command/CommandPoolHelper.php @@ -0,0 +1,57 @@ + + */ +class CommandPoolHelper +{ + public function __construct(private readonly KernelInterface $kernel) + { + } + + /** + * Return all commands indexed by a given type + * @return Command[] + */ + public function getCommands(string $type): array + { + $application = new Application($this->kernel); + + return array_filter($application->all(), function ($key) use ($type) { + return str_starts_with($key, "$type:"); + }, ARRAY_FILTER_USE_KEY); + } + + /** + * Parse commands from a given type and return an associative array that can be used in form choice type + * @return array + * [ + * 'app.my_command.label' => 'my-command', + * 'app.my_other_command.label' => 'my-other-command', + * ] + */ + public function getCommandsChoices(string $type): array + { + return ArrayUtils::flatToMap( + array_keys($this->getCommands($type)), + function ($key) { + return str_replace([':', '-'], ['.', '_'], $key) . '.label'; + }, + function ($value) use ($type) { + return str_replace("$type:", '', $value); + } + ); + } + + public function getCronChoices(): array + { + return $this->getCommandsChoices('cron'); + } +} diff --git a/src/Config/IniOverrideConfig.php b/src/Config/IniOverrideConfig.php index 104fd48..eee8d7a 100644 --- a/src/Config/IniOverrideConfig.php +++ b/src/Config/IniOverrideConfig.php @@ -40,4 +40,15 @@ public function resetMemoryLimit(): void { ini_set('memory_limit', $this->getDefaultMemoryLimit()); } + + /** + * Call this function for all PHP script running from the CLI (ex: all symfony cron commands) when you are on CleverCloud to ensure every date + * creation or comparaison are using the right timezone. + * Alternativaly you can also set it directly when invoking the PHP command using the -d option like so : + * php -d date.timezone="Europe/Paris" bin/console app:my-command + */ + public function initDefaultTimezoneForCli(string $timezone = 'Europe/Paris'): void + { + date_default_timezone_set($timezone); + } } diff --git a/src/Entity/AddressInterface.php b/src/Entity/AddressInterface.php new file mode 100644 index 0000000..9434c95 --- /dev/null +++ b/src/Entity/AddressInterface.php @@ -0,0 +1,14 @@ +address; + } + + public function setAddress(?string $address): self + { + $this->address = $address; + + return $this; + } + + public function getAdditionalAddress(): ?string + { + return $this->additionalAddress; + } + + public function setAdditionalAddress(?string $additionalAddress): self + { + $this->additionalAddress = $additionalAddress; + + return $this; + } + + public function getPostalCode(): ?string + { + return $this->postalCode; + } + + public function setPostalCode(?string $postalCode): self + { + if (strlen($postalCode) === 4) { + $postalCode = '0' . $postalCode; + } + $this->postalCode = $postalCode; + + return $this; + } + + public function getCity(): ?string + { + return $this->city; + } + + public function setCity(?string $city): self + { + $this->city = $city; + + return $this; + } +} diff --git a/src/Entity/ApiCallInterface.php b/src/Entity/ApiCallInterface.php new file mode 100644 index 0000000..c3b9cfc --- /dev/null +++ b/src/Entity/ApiCallInterface.php @@ -0,0 +1,30 @@ +statusCode . ' - ' . $this->getRouteUrl(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getOrigin(): ?string + { + return $this->origin; + } + + public function setOrigin(string $origin): static + { + $this->origin = $origin; + + return $this; + } + + public function getStatusCode(): ?int + { + return $this->statusCode; + } + + public function setStatusCode(int $statusCode): static + { + $this->statusCode = $statusCode; + + return $this; + } + + public function getMethod(): ?string + { + return $this->method; + } + + public function setMethod(string $method): static + { + $this->method = $method; + + return $this; + } + + public function getRouteUrl(): ?string + { + return $this->routeUrl; + } + + public function setRouteUrl(string $routeUrl): static + { + $this->routeUrl = $routeUrl; + + return $this; + } + + public function getInputData(): ?array + { + return $this->inputData; + } + + public function setInputData(?array $inputData): static + { + $this->inputData = $inputData; + + return $this; + } + + public function getOutputResponse(): array|string|null + { + return $this->outputResponse; + } + + public function setOutputResponse(array|string|null $outputResponse): static + { + $this->outputResponse = $outputResponse; + + return $this; + } +} diff --git a/src/Entity/EmailInterface.php b/src/Entity/EmailInterface.php new file mode 100644 index 0000000..4fda2c6 --- /dev/null +++ b/src/Entity/EmailInterface.php @@ -0,0 +1,10 @@ +email; + } + + public function setEmail(?string $email): self + { + $email = strtolower($email); + if ($email === '') { + $email = null; + } + $this->email = $email; + + return $this; + } +} diff --git a/src/Entity/NameableInterface.php b/src/Entity/NameableInterface.php new file mode 100644 index 0000000..a992776 --- /dev/null +++ b/src/Entity/NameableInterface.php @@ -0,0 +1,10 @@ +name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/src/Entity/OrganizationInterface.php b/src/Entity/OrganizationInterface.php new file mode 100644 index 0000000..eb92f9f --- /dev/null +++ b/src/Entity/OrganizationInterface.php @@ -0,0 +1,12 @@ +id; + } + + public function getOrganizationEmail(): ?string + { + return $this->organizationEmail; + } + + public function setOrganizationEmail(?string $organizationEmail): static + { + $this->organizationEmail = $organizationEmail; + + return $this; + } +} diff --git a/src/Entity/PersonNameableInterface.php b/src/Entity/PersonNameableInterface.php new file mode 100644 index 0000000..c31c11b --- /dev/null +++ b/src/Entity/PersonNameableInterface.php @@ -0,0 +1,18 @@ +getFirstName(), $this->getLastName())); + if ($toReturn === '') { + return null; + } + + return $toReturn; + } + + public function getInitial(): string + { + return sprintf( + '%s%s', + substr(trim($this->getFirstName()), 0, 1), + substr(trim($this->getLastName()), 0, 1) + ); + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(?string $firstName): self + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(?string $lastName): self + { + $this->lastName = $lastName; + + return $this; + } +} diff --git a/src/Entity/PhoneInterface.php b/src/Entity/PhoneInterface.php new file mode 100644 index 0000000..84e5106 --- /dev/null +++ b/src/Entity/PhoneInterface.php @@ -0,0 +1,10 @@ +phone; + } + + public function setPhone(?string $phone): static + { + $this->phone = $phone; + + return $this; + } +} diff --git a/src/Entity/ProcessInterface.php b/src/Entity/ProcessInterface.php new file mode 100644 index 0000000..d1eac30 --- /dev/null +++ b/src/Entity/ProcessInterface.php @@ -0,0 +1,56 @@ +getStatus() === ProcessStatusEnum::ONGOING; + } + + public function isSuccess(): bool + { + return $this->getStatus() === ProcessStatusEnum::SUCCESS; + } + + public function isError(): bool + { + return $this->getStatus() === ProcessStatusEnum::ERROR; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + + return $this; + } + + public function getStartedAt(): ?\DateTimeInterface + { + return $this->startedAt; + } + + public function setStartedAt(\DateTimeInterface $startedAt): static + { + $this->startedAt = $startedAt; + + return $this; + } + + public function getEndedAt(): ?\DateTimeInterface + { + return $this->endedAt; + } + + public function setEndedAt(?\DateTimeInterface $endedAt): static + { + $this->endedAt = $endedAt; + + return $this; + } + + public function getDuration(): ?int + { + return $this->duration; + } + + public function getDurationAsString(): ?string + { + return DateUtils::secondsToString($this->duration); + } + + public function setDuration(?int $duration): static + { + $this->duration = $duration; + + return $this; + } + + public function getStatus(): ?ProcessStatusEnum + { + return $this->status; + } + + public function getStatusAsString(): string + { + return $this->status->value; + } + + public function setStatus(ProcessStatusEnum $status): static + { + $this->status = $status; + + return $this; + } + + public function getSummary(): ?string + { + return $this->summary; + } + + public function setSummary(string $summary): static + { + $this->summary = $summary; + + return $this; + } + + public function getLogs(): ?array + { + return $this->logs; + } + + public function getLogsAsHtml(): ?string + { + $logs = $this->getLogs(); + if (empty($logs)) { + return null; + } + + return implode('
', $logs); + } + + public function setLogs(?array $logs): static + { + $this->logs = $logs; + + return $this; + } + + public function addLog(mixed $log): self + { + if ($this->logs == null) { + $this->logs = []; + } + array_unshift($this->logs, $log); + + return $this; + } + + public function getData(): ?array + { + return $this->data; + } + + public function setData(?array $data): static + { + $this->data = $data; + + return $this; + } + + public function addData(string $key, mixed $value): void + { + $this->data[$key] = $value; + } +} diff --git a/src/Entity/SirenInterface.php b/src/Entity/SirenInterface.php new file mode 100644 index 0000000..787a841 --- /dev/null +++ b/src/Entity/SirenInterface.php @@ -0,0 +1,10 @@ +siren; + } + + public function setSiren(?string $siren): static + { + $this->siren = $siren; + + return $this; + } +} diff --git a/src/Entity/WebsiteInterface.php b/src/Entity/WebsiteInterface.php new file mode 100644 index 0000000..cf61bb8 --- /dev/null +++ b/src/Entity/WebsiteInterface.php @@ -0,0 +1,10 @@ +website; + } + + public function setWebsite(?string $website): static + { + $this->website = $website; + + return $this; + } +} diff --git a/src/Enum/ProcessStatusEnum.php b/src/Enum/ProcessStatusEnum.php new file mode 100644 index 0000000..44385f2 --- /dev/null +++ b/src/Enum/ProcessStatusEnum.php @@ -0,0 +1,24 @@ +trans(self::PREFIX_LABEL . $case->value, [], $translationDomain)] = $onlyValue ? $case->value : $case; + } + + return $toReturn; + } +} diff --git a/src/Monitoring/ProcessMonitor.php b/src/Monitoring/ProcessMonitor.php new file mode 100644 index 0000000..1d0e71c --- /dev/null +++ b/src/Monitoring/ProcessMonitor.php @@ -0,0 +1,42 @@ + + */ +class ProcessMonitor +{ + public function __construct(private readonly EntityManagerInterface $entityManager) + { + } + + public function start(ProcessInterface $process): ProcessInterface + { + $process->setStartedAt(new \DateTime()); + $process->setStatus(ProcessStatusEnum::ONGOING); + + return $process; + } + + public function end(ProcessInterface $process, bool $isSuccess = true, bool $flush = true): void + { + $endedAt = new \DateTime(); + $process->setEndedAt($endedAt); + $process->setDuration($endedAt->getTimestamp() - $process->getStartedAt()->getTimestamp()); + if ($isSuccess) { + $process->setStatus(ProcessStatusEnum::SUCCESS); + } else { + $process->setStatus(ProcessStatusEnum::ERROR); + } + + $this->entityManager->persist($process); + if ($flush) { + $this->entityManager->flush(); + } + } +} diff --git a/src/Utils/DateUtils.php b/src/Utils/DateUtils.php index 5f4b6dc..32fb05a 100644 --- a/src/Utils/DateUtils.php +++ b/src/Utils/DateUtils.php @@ -556,4 +556,28 @@ public static function subYears(\DateTime $dateTime, int $yearsNb): \DateTime return $clonedDateTime; } + + public static function secondsToString(?int $value): ?string + { + if ($value === null) { + return null; + } + + $toReturn = ''; + $hours = floor($value / 3600); + $remainingSeconds = $value % 3600; + $minutes = floor($remainingSeconds / 60); + $seconds = $remainingSeconds % 60; + if ($hours > 0) { + $toReturn .= "{$hours}h "; + } + if ($minutes > 0) { + $toReturn .= "{$minutes}m "; + } + if ($toReturn === '' || $seconds > 0) { + $toReturn .= "{$seconds}s"; + } + + return trim($toReturn); + } } diff --git a/src/Utils/RegexUtils.php b/src/Utils/RegexUtils.php index 6b140a0..c3a1a82 100644 --- a/src/Utils/RegexUtils.php +++ b/src/Utils/RegexUtils.php @@ -4,7 +4,7 @@ class RegexUtils { - public const PHONE_PATTERN = '#^(\+[0-9]{2})?[0-9]{10}$#'; + public const PHONE_PATTERN = '#^(\+[0-9]{1})?[0-9]{10}$#'; public const PHONE_MESSAGE = 'phone.regex_error'; public const FAX_MESSAGE = 'fax.regex_error'; diff --git a/tests/Utils/DateUtilsTest.php b/tests/Utils/DateUtilsTest.php index ddac846..ceaf1bb 100644 --- a/tests/Utils/DateUtilsTest.php +++ b/tests/Utils/DateUtilsTest.php @@ -1302,4 +1302,27 @@ public function testSubYears(): void { $this->assertEquals(new \DateTime('2019-10-10 08:00:00'), DateUtils::subYears(new \DateTime('2024-10-10 08:00:00'), 5)); } + + /** + * @dataProvider secondsToStringProvider + */ + public function testSecondsToString(?string $expected, ?int $value): void + { + $this->assertEquals($expected, DateUtils::secondsToString($value)); + } + + public function secondsToStringProvider(): array + { + return [ + 'null value' => [null, null], + '0 seconds' => ['0s', 0], + '10 seconds' => ['10s', 10], + '1 minutes' => ['1m', 60], + '1 minutes and 30 secondes' => ['1m 30s', 90], + '1 hours' => ['1h', 3600], + '1 hours 15 minutes' => ['1h 15m', 4500], + '1 hours 1 minutes and 1 secondes' => ['1h 1m 1s', 3661], + '3 hours' => ['3h', 10800], + ]; + } } diff --git a/tests/Validator/Constraints/RegexValidatorTest.php b/tests/Validator/Constraints/RegexValidatorTest.php index df653ce..79c978d 100644 --- a/tests/Validator/Constraints/RegexValidatorTest.php +++ b/tests/Validator/Constraints/RegexValidatorTest.php @@ -37,7 +37,7 @@ public function validPhoneProvider(): array { return [ 'concatenated number ' => ["0601020304"], - 'number with dial code without space' => ["+330601020304"], + 'number with dial code without space' => ["+33601020304"], ]; } @@ -62,7 +62,7 @@ public function unvalidPhoneProvider(): array 'number with point' => ["06.01.02.03.04"], 'number with separator combination' => ["0601 02-03.04"], 'grouping of digits other than 2' => ["060 102 0304"], - 'missing + for dialing code' => ["330601020304"], + 'missing + for dialing code' => ["33601020304"], 'number with dial code with space' => ["+33 06 01 02 03 04"], ]; }