TaskSchema module

This commit is contained in:
Marek Lenczewski
2026-04-12 15:42:48 +02:00
parent 4e81cea831
commit 5198769de4
57 changed files with 3066 additions and 324 deletions

View File

@@ -38,3 +38,10 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1|haushalt\.ddev\.site)(:[0-9
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###

View File

@@ -19,9 +19,11 @@
"symfony/dotenv": "7.4.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "7.4.*",
"symfony/messenger": "7.4.*",
"symfony/property-access": "7.4.*",
"symfony/property-info": "7.4.*",
"symfony/runtime": "7.4.*",
"symfony/scheduler": "7.4.*",
"symfony/serializer": "7.4.*",
"symfony/validator": "7.4.*",
"symfony/yaml": "7.4.*"

307
backend/composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "36cb2b820400a518223222a28461ff75",
"content-hash": "3faf0714de84c701cf5b3c444ce28e8b",
"packages": [
{
"name": "doctrine/collections",
@@ -1456,6 +1456,54 @@
},
"time": "2021-02-03T23:26:27+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/clock.git",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Clock\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for reading the clock.",
"homepage": "https://github.com/php-fig/clock",
"keywords": [
"clock",
"now",
"psr",
"psr-20",
"time"
],
"support": {
"issues": "https://github.com/php-fig/clock/issues",
"source": "https://github.com/php-fig/clock/tree/1.0.0"
},
"time": "2022-11-25T14:36:26+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
@@ -1789,6 +1837,84 @@
],
"time": "2025-03-13T15:25:07+00:00"
},
{
"name": "symfony/clock",
"version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/clock.git",
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111",
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/clock": "^1.0",
"symfony/polyfill-php83": "^1.28"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/now.php"
],
"psr-4": {
"Symfony\\Component\\Clock\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Decouples applications from the system clock",
"homepage": "https://symfony.com",
"keywords": [
"clock",
"psr20",
"time"
],
"support": {
"source": "https://github.com/symfony/clock/tree/v7.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/config",
"version": "v7.4.7",
@@ -3121,6 +3247,100 @@
],
"time": "2026-03-06T16:33:18+00:00"
},
{
"name": "symfony/messenger",
"version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/messenger.git",
"reference": "ddf5ab29bc0329ece30e16f01c86abb6241e92d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/messenger/zipball/ddf5ab29bc0329ece30e16f01c86abb6241e92d8",
"reference": "ddf5ab29bc0329ece30e16f01c86abb6241e92d8",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/clock": "^6.4|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
"symfony/console": "<7.2",
"symfony/event-dispatcher": "<6.4",
"symfony/event-dispatcher-contracts": "<2.5",
"symfony/framework-bundle": "<6.4",
"symfony/http-kernel": "<7.3",
"symfony/lock": "<7.4",
"symfony/serializer": "<6.4.32|>=7.3,<7.3.10|>=7.4,<7.4.4|>=8.0,<8.0.4"
},
"require-dev": {
"psr/cache": "^1.0|^2.0|^3.0",
"symfony/console": "^7.2|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/http-kernel": "^7.3|^8.0",
"symfony/lock": "^7.4|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/property-access": "^6.4|^7.0|^8.0",
"symfony/rate-limiter": "^6.4|^7.0|^8.0",
"symfony/routing": "^6.4|^7.0|^8.0",
"symfony/serializer": "^6.4.32|~7.3.10|^7.4.4|^8.0.4",
"symfony/service-contracts": "^2.5|^3",
"symfony/stopwatch": "^6.4|^7.0|^8.0",
"symfony/validator": "^6.4|^7.0|^8.0",
"symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Messenger\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Samuel Roze",
"email": "samuel.roze@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps applications send and receive messages to/from other applications or via message queues",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/messenger/tree/v7.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-30T12:55:43+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.33.0",
@@ -3952,6 +4172,91 @@
],
"time": "2025-12-05T14:04:53+00:00"
},
{
"name": "symfony/scheduler",
"version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/scheduler.git",
"reference": "f95e696edaad466db9b087a6480ef936c766c3de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/scheduler/zipball/f95e696edaad466db9b087a6480ef936c766c3de",
"reference": "f95e696edaad466db9b087a6480ef936c766c3de",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/clock": "^6.4|^7.0|^8.0"
},
"require-dev": {
"dragonmantank/cron-expression": "^3.1",
"symfony/cache": "^6.4|^7.0|^8.0",
"symfony/console": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/lock": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/serializer": "^6.4|^7.1|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Scheduler\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sergey Rabochiy",
"email": "upyx.00@gmail.com"
},
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides scheduling through Symfony Messenger",
"homepage": "https://symfony.com",
"keywords": [
"cron",
"schedule",
"scheduler"
],
"support": {
"source": "https://github.com/symfony/scheduler/tree/v7.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/serializer",
"version": "v7.4.7",

View File

@@ -0,0 +1,8 @@
framework:
messenger:
transports:
sync: 'sync://'
scheduler_default: 'scheduler://default'
routing:
App\Message\GenerateTasksMessage: scheduler_default

View File

@@ -426,7 +426,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* resources?: array<string, scalar|Param|null>,
* },
* messenger?: bool|array{ // Messenger configuration
* enabled?: bool|Param, // Default: false
* enabled?: bool|Param, // Default: true
* routing?: array<string, string|array{ // Default: []
* senders?: list<scalar|Param|null>,
* }>,
@@ -468,7 +468,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }>,
* },
* scheduler?: bool|array{ // Scheduler configuration
* enabled?: bool|Param, // Default: false
* enabled?: bool|Param, // Default: true
* },
* disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true
* http_client?: bool|array{ // HTTP Client configuration

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260412094958 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE task_schema (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, task_status VARCHAR(255) NOT NULL, date DATE DEFAULT NULL, `repeat` JSON DEFAULT NULL, start DATE DEFAULT NULL, end DATE DEFAULT NULL, PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('ALTER TABLE task ADD schema_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB25EA1BEF35 FOREIGN KEY (schema_id) REFERENCES task_schema (id) ON DELETE SET NULL');
$this->addSql('CREATE INDEX IDX_527EDB25EA1BEF35 ON task (schema_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE task_schema');
$this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB25EA1BEF35');
$this->addSql('DROP INDEX IDX_527EDB25EA1BEF35 ON task');
$this->addSql('ALTER TABLE task DROP schema_id');
}
}

Binary file not shown.

View File

@@ -1,4 +1,4 @@
{
"versionCode": 1,
"versionCode": 2,
"apkFile": "haushalt.apk"
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Collection;
use App\Entity\TaskSchema;
/**
* @implements \IteratorAggregate<int, TaskSchema>
*/
final class TaskSchemaCollection implements \IteratorAggregate
{
/** @var list<TaskSchema> */
private array $schemas = [];
/**
* @param list<TaskSchema> $schemas
*/
public function __construct(array $schemas = [])
{
foreach ($schemas as $schema) {
$this->schemas[] = $schema;
}
}
public function getIterator(): \Iterator
{
return new \ArrayIterator($this->schemas);
}
}

View File

@@ -45,14 +45,6 @@ class TaskController extends AbstractController
return $this->json($task, 200, [], ['groups' => ['task:read']]);
}
#[Route('', methods: ['POST'])]
public function create(#[Payload] TaskRequest $dto): JsonResponse
{
$task = $this->manager->create($dto);
return $this->json($task, 201, [], ['groups' => ['task:read']]);
}
#[Route('/{id}', methods: ['PUT'], requirements: ['id' => '\d+'])]
public function update(Task $task, #[Payload] TaskRequest $dto): JsonResponse
{

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Controller\Api;
use App\DTO\TaskSchemaRequest;
use App\Entity\TaskSchema;
use App\Repository\TaskSchemaRepository;
use App\Service\TaskSchemaManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload as Payload;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api/task-schemas')]
class TaskSchemaController extends AbstractController
{
public function __construct(
private readonly TaskSchemaRepository $repo,
private readonly TaskSchemaManager $manager,
) {
}
#[Route('', methods: ['GET'])]
public function index(): JsonResponse
{
$schemas = $this->repo->allSchemas();
return $this->json($schemas, 200, [], ['groups' => ['task_schema:read']]);
}
#[Route('/{id}', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(TaskSchema $schema): JsonResponse
{
return $this->json($schema, 200, [], ['groups' => ['task_schema:read']]);
}
#[Route('', methods: ['POST'])]
public function create(#[Payload] TaskSchemaRequest $dto): JsonResponse
{
$this->manager->create($dto);
return new JsonResponse(null, 201);
}
#[Route('/{id}', methods: ['PUT'], requirements: ['id' => '\d+'])]
public function update(TaskSchema $schema, #[Payload] TaskSchemaRequest $dto): JsonResponse
{
$this->manager->update($schema, $dto);
return $this->json($schema, 200, [], ['groups' => ['task_schema:read']]);
}
#[Route('/{id}', methods: ['DELETE'], requirements: ['id' => '\d+'])]
public function delete(TaskSchema $schema): JsonResponse
{
$this->manager->delete($schema);
return new JsonResponse(null, 204);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\DTO;
use App\Enum\TaskSchemaStatus;
use App\Enum\TaskStatus;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Validator\Constraints as Assert;
class TaskSchemaRequest
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Length(max: 255)]
public readonly string $name,
#[Assert\NotNull]
public readonly TaskSchemaStatus $status = TaskSchemaStatus::Active,
#[Assert\NotNull]
public readonly TaskStatus $taskStatus = TaskStatus::Active,
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
public readonly ?\DateTimeImmutable $date = null,
public readonly ?array $repeat = null,
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
public readonly ?\DateTimeImmutable $start = null,
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
public readonly ?\DateTimeImmutable $end = null,
) {
}
}

View File

@@ -6,6 +6,7 @@ use App\Enum\TaskStatus;
use App\Repository\TaskRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\Ignore;
#[ORM\Entity(repositoryClass: TaskRepository::class)]
class Task
@@ -27,6 +28,11 @@ class Task
#[ORM\Column(enumType: TaskStatus::class)]
private TaskStatus $status = TaskStatus::Active;
#[ORM\ManyToOne(targetEntity: TaskSchema::class, inversedBy: 'tasks')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Ignore]
private ?TaskSchema $schema = null;
public function getId(): ?int
{
return $this->id;
@@ -77,4 +83,16 @@ class Task
return $this;
}
public function getSchema(): ?TaskSchema
{
return $this->schema;
}
public function setSchema(?TaskSchema $schema): self
{
$this->schema = $schema;
return $this;
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace App\Entity;
use App\Enum\TaskSchemaStatus;
use App\Enum\TaskStatus;
use App\Repository\TaskSchemaRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: TaskSchemaRepository::class)]
class TaskSchema
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_schema:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_schema:read'])]
private string $name = '';
#[ORM\Column(enumType: TaskSchemaStatus::class)]
#[Groups(['task_schema:read'])]
private TaskSchemaStatus $status = TaskSchemaStatus::Active;
#[ORM\Column(enumType: TaskStatus::class)]
#[Groups(['task_schema:read'])]
private TaskStatus $taskStatus = TaskStatus::Active;
#[ORM\Column(type: 'date_immutable', nullable: true)]
#[Groups(['task_schema:read'])]
private ?\DateTimeImmutable $date = null;
#[ORM\Column(name: '`repeat`', type: 'json', nullable: true)]
#[Groups(['task_schema:read'])]
private ?array $repeat = null;
#[ORM\Column(name: '`start`', type: 'date_immutable', nullable: true)]
#[Groups(['task_schema:read'])]
private ?\DateTimeImmutable $start = null;
#[ORM\Column(name: '`end`', type: 'date_immutable', nullable: true)]
#[Groups(['task_schema:read'])]
private ?\DateTimeImmutable $end = null;
/** @var Collection<int, Task> */
#[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'schema')]
private Collection $tasks;
public function __construct()
{
$this->tasks = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getStatus(): TaskSchemaStatus
{
return $this->status;
}
public function setStatus(TaskSchemaStatus $status): self
{
$this->status = $status;
return $this;
}
public function getTaskStatus(): TaskStatus
{
return $this->taskStatus;
}
public function setTaskStatus(TaskStatus $taskStatus): self
{
$this->taskStatus = $taskStatus;
return $this;
}
public function getDate(): ?\DateTimeImmutable
{
return $this->date;
}
public function setDate(?\DateTimeImmutable $date): self
{
$this->date = $date;
return $this;
}
public function getRepeat(): ?array
{
return $this->repeat;
}
public function getRepeatType(): ?string
{
return $this->repeat !== null ? array_key_first($this->repeat) : null;
}
public function setRepeat(?array $repeat): self
{
$this->repeat = $repeat;
return $this;
}
public function getStart(): ?\DateTimeImmutable
{
return $this->start;
}
public function setStart(?\DateTimeImmutable $start): self
{
$this->start = $start;
return $this;
}
public function getEnd(): ?\DateTimeImmutable
{
return $this->end;
}
public function setEnd(?\DateTimeImmutable $end): self
{
$this->end = $end;
return $this;
}
/** @return Collection<int, Task> */
public function getTasks(): Collection
{
return $this->tasks;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Enum;
enum TaskSchemaStatus: string
{
case Active = 'active';
case Inactive = 'inactive';
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Message;
final class GenerateTasksMessage
{
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\MessageHandler;
use App\Message\GenerateTasksMessage;
use App\Service\TaskGenerator;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class GenerateTasksMessageHandler
{
public function __construct(private TaskGenerator $generator)
{
}
public function __invoke(GenerateTasksMessage $message): void
{
$this->generator->generateNewTasks();
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Repository;
use App\Collection\TaskSchemaCollection;
use App\Entity\TaskSchema;
use App\Enum\TaskSchemaStatus;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TaskSchema>
*/
class TaskSchemaRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TaskSchema::class);
}
public function allSchemas(): TaskSchemaCollection
{
return new TaskSchemaCollection(parent::findAll());
}
/** @return list<TaskSchema> */
public function findActiveWithRepeat(): array
{
return $this->createQueryBuilder('s')
->andWhere('s.status = :status')
->andWhere('s.repeat IS NOT NULL')
->setParameter('status', TaskSchemaStatus::Active)
->getQuery()
->getResult();
}
}

27
backend/src/Schedule.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
namespace App;
use App\Message\GenerateTasksMessage;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule as SymfonySchedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
use Symfony\Contracts\Cache\CacheInterface;
#[AsSchedule]
class Schedule implements ScheduleProviderInterface
{
public function __construct(
private CacheInterface $cache,
) {
}
public function getSchedule(): SymfonySchedule
{
return (new SymfonySchedule())
->stateful($this->cache)
->processOnlyLastMissedRun(true)
->add(RecurringMessage::cron('0 3 * * *', new GenerateTasksMessage()));
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Service;
use App\Entity\Task;
use App\Entity\TaskSchema;
use App\Enum\TaskStatus;
use App\Repository\TaskSchemaRepository;
use Doctrine\ORM\EntityManagerInterface;
class TaskGenerator
{
public function __construct(
private EntityManagerInterface $em,
private TaskSchemaRepository $schemaRepo,
) {
}
public function generateNewTasks(): void
{
$schemas = $this->schemaRepo->findActiveWithRepeat();
foreach ($schemas as $schema) {
$this->removeTasks($schema);
$this->generateTasks($schema);
}
$this->em->flush();
}
public function removeTasks(TaskSchema $schema): void
{
foreach ($schema->getTasks() as $task) {
if ($task->getStatus() === TaskStatus::Past) continue;
$this->em->remove($task);
}
}
public function generateTasks(TaskSchema $schema): void
{
$dates = $this->getDates($schema);
foreach ($dates as $date) {
$task = new Task();
$task->setName($schema->getName());
$task->setDate($date);
$task->setStatus($schema->getTaskStatus());
$task->setSchema($schema);
$this->em->persist($task);
}
}
/** @return array{\DateTimeImmutable, \DateTimeImmutable} */
private function getDateRange(TaskSchema $schema): array
{
$today = new \DateTimeImmutable('today');
$from = max($today, $schema->getStart() ?? $today);
$end = min($today->modify('+14 days'), $schema->getEnd() ?? $today->modify('+14 days'));
return [$from, $end];
}
/** @return list<\DateTimeImmutable> */
private function getDates(TaskSchema $schema): array
{
[$from, $end] = $this->getDateRange($schema);
$type = $schema->getRepeatType();
$repeat = $schema->getRepeat();
$dates = [];
for ($date = $from; $date <= $end; $date = $date->modify('+1 day')) {
if ($type === 'weekly') {
$weekday = (int) $date->format('N') - 1;
if(!$repeat['weekly'][$weekday]) continue;
}
if ($type === 'monthly') {
$monthday = (int) $date->format('j') - 1;
if(!$repeat['monthly'][$monthday]) continue;
}
$dates[] = $date;
}
return $dates;
}
}

View File

@@ -13,19 +13,6 @@ class TaskManager
{
}
public function create(TaskRequest $req): Task
{
$task = new Task();
$task->setName($req->name);
$task->setDate($req->date);
$task->setStatus($req->status);
$this->em->persist($task);
$this->em->flush();
return $task;
}
public function update(Task $task, TaskRequest $req): Task
{
$task->setName($req->name);

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Service;
use App\DTO\TaskSchemaRequest;
use App\Entity\Task;
use App\Entity\TaskSchema;
use App\Enum\TaskSchemaStatus;
use Doctrine\ORM\EntityManagerInterface;
class TaskSchemaManager
{
public function __construct(
private EntityManagerInterface $em,
private TaskGenerator $generator,
) {
}
public function create(TaskSchemaRequest $req): void
{
if ($req->repeat === null) {
$task = new Task();
$task->setName($req->name);
$task->setDate($req->date);
$task->setStatus($req->taskStatus);
$this->em->persist($task);
$this->em->flush();
return;
}
$schema = new TaskSchema();
$schema->setName($req->name);
$schema->setStatus($req->status);
$schema->setTaskStatus($req->taskStatus);
$schema->setDate($req->date);
$schema->setRepeat($req->repeat);
$schema->setStart($req->start);
$schema->setEnd($req->end);
$this->em->persist($schema);
$this->em->flush();
if ($schema->getStatus() === TaskSchemaStatus::Inactive) {
return;
}
$this->generator->generateTasks($schema);
$this->em->flush();
}
public function update(TaskSchema $schema, TaskSchemaRequest $req): void
{
$schema->setName($req->name);
$schema->setStatus($req->status);
$schema->setTaskStatus($req->taskStatus);
$schema->setDate($req->date);
$schema->setRepeat($req->repeat);
$schema->setStart($req->start);
$schema->setEnd($req->end);
if ($schema->getStatus() === TaskSchemaStatus::Inactive) {
$this->em->flush();
return;
}
$this->generator->removeTasks($schema);
$this->generator->generateTasks($schema);
$this->em->flush();
}
public function delete(TaskSchema $schema): void
{
$this->generator->removeTasks($schema);
$this->em->remove($schema);
$this->em->flush();
}
}

View File

@@ -101,6 +101,18 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/messenger": {
"version": "7.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.0",
"ref": "d8936e2e2230637ef97e5eecc0eea074eecae58b"
},
"files": [
"config/packages/messenger.yaml"
]
},
"symfony/property-info": {
"version": "7.4",
"recipe": {
@@ -126,6 +138,18 @@
"config/routes.yaml"
]
},
"symfony/scheduler": {
"version": "7.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "caea3c928ee9e1b21288fd76aef36f16ea355515"
},
"files": [
"src/Schedule.php"
]
},
"symfony/validator": {
"version": "7.4",
"recipe": {

View File

@@ -13,6 +13,7 @@ return array(
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
);

View File

@@ -28,11 +28,13 @@ return array(
'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
'Symfony\\Component\\Stopwatch\\' => array($vendorDir . '/symfony/stopwatch'),
'Symfony\\Component\\Serializer\\' => array($vendorDir . '/symfony/serializer'),
'Symfony\\Component\\Scheduler\\' => array($vendorDir . '/symfony/scheduler'),
'Symfony\\Component\\Runtime\\' => array($vendorDir . '/symfony/runtime'),
'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'),
'Symfony\\Component\\PropertyInfo\\' => array($vendorDir . '/symfony/property-info'),
'Symfony\\Component\\PropertyAccess\\' => array($vendorDir . '/symfony/property-access'),
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
'Symfony\\Component\\Messenger\\' => array($vendorDir . '/symfony/messenger'),
'Symfony\\Component\\HttpKernel\\' => array($vendorDir . '/symfony/http-kernel'),
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
@@ -43,6 +45,7 @@ return array(
'Symfony\\Component\\DependencyInjection\\' => array($vendorDir . '/symfony/dependency-injection'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Symfony\\Component\\Config\\' => array($vendorDir . '/symfony/config'),
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'),
'Symfony\\Bundle\\MakerBundle\\' => array($vendorDir . '/symfony/maker-bundle/src'),
'Symfony\\Bundle\\FrameworkBundle\\' => array($vendorDir . '/symfony/framework-bundle'),
@@ -50,6 +53,7 @@ return array(
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PHPStan\\PhpDocParser\\' => array($vendorDir . '/phpstan/phpdoc-parser/src'),

View File

@@ -14,8 +14,9 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
'9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php',
'2203a247e6fda86070a5e4e07aed533a' => __DIR__ . '/..' . '/symfony/clock/Resources/now.php',
);
public static $prefixLengthsPsr4 = array (
@@ -49,11 +50,13 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
'Symfony\\Component\\String\\' => 25,
'Symfony\\Component\\Stopwatch\\' => 28,
'Symfony\\Component\\Serializer\\' => 29,
'Symfony\\Component\\Scheduler\\' => 28,
'Symfony\\Component\\Runtime\\' => 26,
'Symfony\\Component\\Routing\\' => 26,
'Symfony\\Component\\PropertyInfo\\' => 31,
'Symfony\\Component\\PropertyAccess\\' => 33,
'Symfony\\Component\\Process\\' => 26,
'Symfony\\Component\\Messenger\\' => 28,
'Symfony\\Component\\HttpKernel\\' => 29,
'Symfony\\Component\\HttpFoundation\\' => 33,
'Symfony\\Component\\Finder\\' => 25,
@@ -64,6 +67,7 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
'Symfony\\Component\\DependencyInjection\\' => 38,
'Symfony\\Component\\Console\\' => 26,
'Symfony\\Component\\Config\\' => 25,
'Symfony\\Component\\Clock\\' => 24,
'Symfony\\Component\\Cache\\' => 24,
'Symfony\\Bundle\\MakerBundle\\' => 27,
'Symfony\\Bundle\\FrameworkBundle\\' => 31,
@@ -74,6 +78,7 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
'Psr\\Log\\' => 8,
'Psr\\EventDispatcher\\' => 20,
'Psr\\Container\\' => 14,
'Psr\\Clock\\' => 10,
'Psr\\Cache\\' => 10,
'PhpParser\\' => 10,
'PHPStan\\PhpDocParser\\' => 21,
@@ -196,6 +201,10 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
array (
0 => __DIR__ . '/..' . '/symfony/serializer',
),
'Symfony\\Component\\Scheduler\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/scheduler',
),
'Symfony\\Component\\Runtime\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/runtime',
@@ -216,6 +225,10 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
array (
0 => __DIR__ . '/..' . '/symfony/process',
),
'Symfony\\Component\\Messenger\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/messenger',
),
'Symfony\\Component\\HttpKernel\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/http-kernel',
@@ -256,6 +269,10 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
array (
0 => __DIR__ . '/..' . '/symfony/config',
),
'Symfony\\Component\\Clock\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/clock',
),
'Symfony\\Component\\Cache\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/cache',
@@ -284,6 +301,10 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
array (
0 => __DIR__ . '/..' . '/psr/container/src',
),
'Psr\\Clock\\' =>
array (
0 => __DIR__ . '/..' . '/psr/clock/src',
),
'Psr\\Cache\\' =>
array (
0 => __DIR__ . '/..' . '/psr/cache/src',

View File

@@ -1568,6 +1568,57 @@
},
"install-path": "../psr/cache"
},
{
"name": "psr/clock",
"version": "1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/clock.git",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0"
},
"time": "2022-11-25T14:36:26+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Clock\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for reading the clock.",
"homepage": "https://github.com/php-fig/clock",
"keywords": [
"clock",
"now",
"psr",
"psr-20",
"time"
],
"support": {
"issues": "https://github.com/php-fig/clock/issues",
"source": "https://github.com/php-fig/clock/tree/1.0.0"
},
"install-path": "../psr/clock"
},
{
"name": "psr/container",
"version": "2.0.2",
@@ -1916,6 +1967,87 @@
],
"install-path": "../symfony/cache-contracts"
},
{
"name": "symfony/clock",
"version": "v7.4.8",
"version_normalized": "7.4.8.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/clock.git",
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111",
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/clock": "^1.0",
"symfony/polyfill-php83": "^1.28"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"time": "2026-03-24T13:12:05+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"Resources/now.php"
],
"psr-4": {
"Symfony\\Component\\Clock\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Decouples applications from the system clock",
"homepage": "https://symfony.com",
"keywords": [
"clock",
"psr20",
"time"
],
"support": {
"source": "https://github.com/symfony/clock/tree/v7.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/clock"
},
{
"name": "symfony/config",
"version": "v7.4.7",
@@ -3395,6 +3527,103 @@
],
"install-path": "../symfony/maker-bundle"
},
{
"name": "symfony/messenger",
"version": "v7.4.8",
"version_normalized": "7.4.8.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/messenger.git",
"reference": "ddf5ab29bc0329ece30e16f01c86abb6241e92d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/messenger/zipball/ddf5ab29bc0329ece30e16f01c86abb6241e92d8",
"reference": "ddf5ab29bc0329ece30e16f01c86abb6241e92d8",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/clock": "^6.4|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
"symfony/console": "<7.2",
"symfony/event-dispatcher": "<6.4",
"symfony/event-dispatcher-contracts": "<2.5",
"symfony/framework-bundle": "<6.4",
"symfony/http-kernel": "<7.3",
"symfony/lock": "<7.4",
"symfony/serializer": "<6.4.32|>=7.3,<7.3.10|>=7.4,<7.4.4|>=8.0,<8.0.4"
},
"require-dev": {
"psr/cache": "^1.0|^2.0|^3.0",
"symfony/console": "^7.2|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/http-kernel": "^7.3|^8.0",
"symfony/lock": "^7.4|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/property-access": "^6.4|^7.0|^8.0",
"symfony/rate-limiter": "^6.4|^7.0|^8.0",
"symfony/routing": "^6.4|^7.0|^8.0",
"symfony/serializer": "^6.4.32|~7.3.10|^7.4.4|^8.0.4",
"symfony/service-contracts": "^2.5|^3",
"symfony/stopwatch": "^6.4|^7.0|^8.0",
"symfony/validator": "^6.4|^7.0|^8.0",
"symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"time": "2026-03-30T12:55:43+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\Messenger\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Samuel Roze",
"email": "samuel.roze@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps applications send and receive messages to/from other applications or via message queues",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/messenger/tree/v7.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/messenger"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.33.0",
@@ -4324,6 +4553,94 @@
],
"install-path": "../symfony/runtime"
},
{
"name": "symfony/scheduler",
"version": "v7.4.8",
"version_normalized": "7.4.8.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/scheduler.git",
"reference": "f95e696edaad466db9b087a6480ef936c766c3de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/scheduler/zipball/f95e696edaad466db9b087a6480ef936c766c3de",
"reference": "f95e696edaad466db9b087a6480ef936c766c3de",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/clock": "^6.4|^7.0|^8.0"
},
"require-dev": {
"dragonmantank/cron-expression": "^3.1",
"symfony/cache": "^6.4|^7.0|^8.0",
"symfony/console": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/lock": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/serializer": "^6.4|^7.1|^8.0"
},
"time": "2026-03-24T13:12:05+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\Scheduler\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sergey Rabochiy",
"email": "upyx.00@gmail.com"
},
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides scheduling through Symfony Messenger",
"homepage": "https://symfony.com",
"keywords": [
"cron",
"schedule",
"scheduler"
],
"support": {
"source": "https://github.com/symfony/scheduler/tree/v7.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/scheduler"
},
{
"name": "symfony/serializer",
"version": "v7.4.7",

View File

@@ -3,7 +3,7 @@
'name' => 'symfony/skeleton',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '2f96caaa233f92fb18ffcdc3f13805d0e7e41369',
'reference' => '4e81cea8317097bcd2f8ce85decd6b78e5156e8a',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -196,6 +196,21 @@
0 => '2.0|3.0',
),
),
'psr/clock' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/clock',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/clock-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
),
),
'psr/container' => array(
'pretty_version' => '2.0.2',
'version' => '2.0.2.0',
@@ -271,6 +286,15 @@
0 => '1.1|2.0|3.0',
),
),
'symfony/clock' => array(
'pretty_version' => 'v7.4.8',
'version' => '7.4.8.0',
'reference' => '674fa3b98e21531dd040e613479f5f6fa8f32111',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/clock',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/config' => array(
'pretty_version' => 'v7.4.7',
'version' => '7.4.7.0',
@@ -421,6 +445,15 @@
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/messenger' => array(
'pretty_version' => 'v7.4.8',
'version' => '7.4.8.0',
'reference' => 'ddf5ab29bc0329ece30e16f01c86abb6241e92d8',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/messenger',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-ctype' => array(
'dev_requirement' => false,
'replaced' => array(
@@ -568,6 +601,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/scheduler' => array(
'pretty_version' => 'v7.4.8',
'version' => '7.4.8.0',
'reference' => 'f95e696edaad466db9b087a6480ef936c766c3de',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/scheduler',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/serializer' => array(
'pretty_version' => 'v7.4.7',
'version' => '7.4.7.0',
@@ -595,7 +637,7 @@
'symfony/skeleton' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '2f96caaa233f92fb18ffcdc3f13805d0e7e41369',
'reference' => '4e81cea8317097bcd2f8ce85decd6b78e5156e8a',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),