createFixtures(); } protected function tearDown(): void { $container = static::getContainer(); /** @var Connection $connection */ $connection = $container->get(Connection::class); $connection->executeStatement('DELETE FROM homework WHERE tenant_id = :tid', ['tid' => self::TENANT_ID]); $connection->executeStatement('DELETE FROM homework_rules WHERE tenant_id = :tid', ['tid' => self::TENANT_ID]); $connection->executeStatement('DELETE FROM school_classes WHERE id IN (:id1, :id2)', ['id1' => self::CLASS_ID, 'id2' => self::TARGET_CLASS_ID]); $connection->executeStatement('DELETE FROM subjects WHERE id = :id', ['id' => self::SUBJECT_ID]); $connection->executeStatement('DELETE FROM users WHERE id IN (:o, :t)', ['o' => self::OWNER_TEACHER_ID, 't' => self::OTHER_TEACHER_ID]); parent::tearDown(); } private function createFixtures(): void { $container = static::getContainer(); /** @var Connection $connection */ $connection = $container->get(Connection::class); $schoolId = '550e8400-e29b-41d4-a716-ff6655440001'; $academicYearId = '550e8400-e29b-41d4-a716-ff6655440002'; $connection->executeStatement( "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at) VALUES (:id, :tid, 'owner-hw@test.local', '', 'Owner', 'Teacher', '[\"ROLE_PROF\"]', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::OWNER_TEACHER_ID, 'tid' => self::TENANT_ID], ); $connection->executeStatement( "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at) VALUES (:id, :tid, 'other-hw@test.local', '', 'Other', 'Teacher', '[\"ROLE_PROF\"]', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::OTHER_TEACHER_ID, 'tid' => self::TENANT_ID], ); $connection->executeStatement( "INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, status, created_at, updated_at) VALUES (:id, :tid, :sid, :ayid, 'Test-HW-Class', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::CLASS_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId, 'ayid' => $academicYearId], ); $connection->executeStatement( "INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) VALUES (:id, :tid, :sid, 'Test-HW-Subject', 'THW', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::SUBJECT_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId], ); $connection->executeStatement( "INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, status, created_at, updated_at) VALUES (:id, :tid, :sid, :ayid, 'Test-HW-Target-Class', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::TARGET_CLASS_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId, 'ayid' => $academicYearId], ); } // ========================================================================= // Security - Without tenant (404) // ========================================================================= #[Test] public function getHomeworkListReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('GET', '/api/homework', [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function createHomeworkReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('POST', '/api/homework', [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir de maths', 'dueDate' => '2026-06-15', ], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function updateHomeworkReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('PATCH', '/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => [ 'title' => 'Titre modifié', 'dueDate' => '2026-06-20', ], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function deleteHomeworkReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('DELETE', '/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(404); } // ========================================================================= // Security - Without authentication (401) // ========================================================================= #[Test] public function getHomeworkListReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('GET', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function createHomeworkReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir de maths', 'dueDate' => '2026-06-15', ], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function updateHomeworkReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('PATCH', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => [ 'title' => 'Titre modifié', 'dueDate' => '2026-06-20', ], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function deleteHomeworkReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('DELETE', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(401); } // ========================================================================= // 5.1-FUNC-005 (P1) - POST /homework with empty title -> 422 // ========================================================================= #[Test] public function createHomeworkWithEmptyTitleReturns422(): void { $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => '', 'dueDate' => '2026-06-15', ], ]); self::assertResponseStatusCodeSame(422); } // ========================================================================= // 5.1-FUNC-006 (P1) - POST /homework with past due date -> 400 // ========================================================================= #[Test] public function createHomeworkWithPastDueDateReturns400(): void { $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir de maths', 'dueDate' => '2020-01-01', ], ]); self::assertResponseStatusCodeSame(400); } // ========================================================================= // 5.1-FUNC-007 (P0) - PATCH /homework/{id} by non-owner -> 403 // ========================================================================= #[Test] public function updateHomeworkByNonOwnerReturns403(): void { $this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED); $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('PATCH', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => [ 'title' => 'Titre piraté', 'dueDate' => '2026-06-20', ], ]); self::assertResponseStatusCodeSame(403); } // ========================================================================= // 5.1-FUNC-008 (P1) - PATCH /homework/{id} on deleted homework -> 400 // ========================================================================= #[Test] public function updateDeletedHomeworkReturns400(): void { $this->persistHomework(self::DELETED_HOMEWORK_ID, HomeworkStatus::DELETED); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('PATCH', 'http://ecole-alpha.classeo.local/api/homework/' . self::DELETED_HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => [ 'title' => 'Titre modifié', 'dueDate' => '2026-06-20', ], ]); self::assertResponseStatusCodeSame(400); } // ========================================================================= // 5.1-FUNC-009 (P0) - DELETE /homework/{id} by non-owner -> 403 // ========================================================================= #[Test] public function deleteHomeworkByNonOwnerReturns403(): void { $this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED); $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('DELETE', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(403); } // ========================================================================= // 5.1-FUNC-010 (P1) - DELETE /homework/{id} on already deleted -> 400 // ========================================================================= #[Test] public function deleteAlreadyDeletedHomeworkReturns400(): void { $this->persistHomework(self::DELETED_HOMEWORK_ID, HomeworkStatus::DELETED); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('DELETE', 'http://ecole-alpha.classeo.local/api/homework/' . self::DELETED_HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(400); } // ========================================================================= // 5.2-FUNC-001 (P1) - POST /homework/{id}/duplicate without tenant -> 404 // ========================================================================= #[Test] public function duplicateHomeworkReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('POST', '/api/homework/' . self::HOMEWORK_ID . '/duplicate', [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [self::TARGET_CLASS_ID], ], ]); self::assertResponseStatusCodeSame(404); } // ========================================================================= // 5.2-FUNC-002 (P1) - POST /homework/{id}/duplicate without auth -> 401 // ========================================================================= #[Test] public function duplicateHomeworkReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID . '/duplicate', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [self::TARGET_CLASS_ID], ], ]); self::assertResponseStatusCodeSame(401); } // ========================================================================= // 5.2-FUNC-003 (P1) - POST /homework/{id}/duplicate empty classes -> 400 // ========================================================================= #[Test] public function duplicateHomeworkReturns400WithEmptyTargetClasses(): void { $this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID . '/duplicate', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [], ], ]); self::assertResponseStatusCodeSame(400); } // ========================================================================= // 5.2-FUNC-004 (P1) - POST /homework/{id}/duplicate not found -> 404 // ========================================================================= #[Test] public function duplicateHomeworkReturns404WhenHomeworkNotFound(): void { $nonExistentId = '00000000-0000-0000-0000-000000000099'; $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework/' . $nonExistentId . '/duplicate', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [self::TARGET_CLASS_ID], ], ]); self::assertResponseStatusCodeSame(404); } // ========================================================================= // 5.2-FUNC-005 (P1) - POST /homework/{id}/duplicate non-owner -> 404 // ========================================================================= #[Test] public function duplicateHomeworkReturns404WhenTeacherNotOwner(): void { $this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED); $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID . '/duplicate', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [self::TARGET_CLASS_ID], ], ]); self::assertResponseStatusCodeSame(404); } // ========================================================================= // 5.4-FUNC-001 (P2) - GET /homework/{id} with rule override -> hasRuleOverride = true // ========================================================================= #[Test] public function getHomeworkShowsHasRuleOverrideTrue(): void { $this->persistHomeworkWithRuleOverride(self::HOMEWORK_ID); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('GET', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(200); self::assertJsonContains(['hasRuleOverride' => true]); } // ========================================================================= // 5.4-FUNC-002 (P2) - GET /homework/{id} without rule override -> hasRuleOverride = false // ========================================================================= #[Test] public function getHomeworkShowsHasRuleOverrideFalse(): void { $this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('GET', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(200); self::assertJsonContains(['hasRuleOverride' => false]); } // ========================================================================= // 5.4-FUNC-003 (P1) - POST /homework with soft rules violated → 409 with warnings // ========================================================================= #[Test] public function createHomeworkReturns409WhenSoftRulesViolated(): void { $this->persistSoftRulesWithMinimumDelay(7); $this->seedTeacherAssignment(); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); // Due date tomorrow = violates 7-day minimum_delay $tomorrow = (new DateTimeImmutable('+1 weekday'))->format('Y-m-d'); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir test 409', 'dueDate' => $tomorrow, ], ]); self::assertResponseStatusCodeSame(409); $json = $client->getResponse()->toArray(false); self::assertSame('homework_rules_warning', $json['type']); self::assertNotEmpty($json['warnings']); self::assertSame('minimum_delay', $json['warnings'][0]['ruleType']); } // ========================================================================= // 5.4-FUNC-004 (P1) - POST /homework with acknowledgeWarning → 201 created // ========================================================================= #[Test] public function createHomeworkReturns201WhenSoftRulesAcknowledged(): void { $this->persistSoftRulesWithMinimumDelay(7); $this->seedTeacherAssignment(); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $tomorrow = (new DateTimeImmutable('+1 weekday'))->format('Y-m-d'); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir acknowledge', 'dueDate' => $tomorrow, 'acknowledgeWarning' => true, ], ]); self::assertResponseStatusCodeSame(201); self::assertJsonContains([ 'title' => 'Devoir acknowledge', 'hasRuleOverride' => true, ]); } // ========================================================================= // 5.5-FUNC-001 (P0) - POST /homework with hard rules violated → 422 with blocked response // ========================================================================= #[Test] public function createHomeworkReturns422WhenHardRulesViolated(): void { $this->persistHardRulesWithMinimumDelay(7); $this->seedTeacherAssignment(); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $tomorrow = (new DateTimeImmutable('+1 weekday'))->format('Y-m-d'); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir test 422', 'dueDate' => $tomorrow, ], ]); self::assertResponseStatusCodeSame(422); $json = $client->getResponse()->toArray(false); self::assertSame('homework_rules_blocked', $json['type']); self::assertNotEmpty($json['warnings']); self::assertSame('minimum_delay', $json['warnings'][0]['ruleType']); self::assertArrayHasKey('suggestedDates', $json); self::assertArrayHasKey('exceptionRequestPath', $json); } // ========================================================================= // 5.5-FUNC-002 (P0) - POST /homework with hard rules violated + acknowledgeWarning → still 422 // ========================================================================= #[Test] public function createHomeworkReturns422EvenWhenHardRulesAcknowledged(): void { $this->persistHardRulesWithMinimumDelay(7); $this->seedTeacherAssignment(); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $tomorrow = (new DateTimeImmutable('+1 weekday'))->format('Y-m-d'); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir bypass hard', 'dueDate' => $tomorrow, 'acknowledgeWarning' => true, ], ]); self::assertResponseStatusCodeSame(422); $json = $client->getResponse()->toArray(false); self::assertSame('homework_rules_blocked', $json['type']); } // ========================================================================= // Helpers // ========================================================================= private function createAuthenticatedClient(string $userId, array $roles): \ApiPlatform\Symfony\Bundle\Test\Client { $client = static::createClient(); $user = new SecurityUser( userId: UserId::fromString($userId), email: 'teacher@classeo.local', hashedPassword: '', tenantId: TenantId::fromString(self::TENANT_ID), roles: $roles, ); $client->loginUser($user, 'api'); return $client; } private function persistSoftRulesWithMinimumDelay(int $days): void { /** @var Connection $connection */ $connection = static::getContainer()->get(Connection::class); $rulesJson = json_encode([['type' => 'minimum_delay', 'params' => ['days' => $days]]], JSON_THROW_ON_ERROR); $connection->executeStatement( "INSERT INTO homework_rules (id, tenant_id, rules, enforcement_mode, enabled, created_at, updated_at) VALUES (gen_random_uuid(), :tid, :rules::jsonb, 'soft', true, NOW(), NOW()) ON CONFLICT (tenant_id) DO UPDATE SET rules = :rules::jsonb, enforcement_mode = 'soft', enabled = true, updated_at = NOW()", ['tid' => self::TENANT_ID, 'rules' => $rulesJson], ); } private function persistHardRulesWithMinimumDelay(int $days): void { /** @var Connection $connection */ $connection = static::getContainer()->get(Connection::class); $rulesJson = json_encode([['type' => 'minimum_delay', 'params' => ['days' => $days]]], JSON_THROW_ON_ERROR); $connection->executeStatement( "INSERT INTO homework_rules (id, tenant_id, rules, enforcement_mode, enabled, created_at, updated_at) VALUES (gen_random_uuid(), :tid, :rules::jsonb, 'hard', true, NOW(), NOW()) ON CONFLICT (tenant_id) DO UPDATE SET rules = :rules::jsonb, enforcement_mode = 'hard', enabled = true, updated_at = NOW()", ['tid' => self::TENANT_ID, 'rules' => $rulesJson], ); } private function seedTeacherAssignment(): void { /** @var Connection $connection */ $connection = static::getContainer()->get(Connection::class); // Compute the current academic year UUID the same way CurrentAcademicYearResolver does $month = (int) date('n'); $year = (int) date('Y'); $startYear = $month >= 9 ? $year : $year - 1; $academicYearId = Uuid::uuid5( '6ba7b814-9dad-11d1-80b4-00c04fd430c8', self::TENANT_ID . ':' . $startYear . '-' . ($startYear + 1), )->toString(); $connection->executeStatement( "INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, status, start_date, created_at, updated_at) VALUES (gen_random_uuid(), :tid, :teacher, :class, :subject, :ayid, 'active', NOW(), NOW(), NOW()) ON CONFLICT DO NOTHING", [ 'tid' => self::TENANT_ID, 'teacher' => self::OWNER_TEACHER_ID, 'class' => self::CLASS_ID, 'subject' => self::SUBJECT_ID, 'ayid' => $academicYearId, ], ); } private function persistHomeworkWithRuleOverride(string $homeworkId): void { $now = new DateTimeImmutable(); $homework = Homework::reconstitute( id: HomeworkId::fromString($homeworkId), tenantId: TenantId::fromString(self::TENANT_ID), classId: ClassId::fromString(self::CLASS_ID), subjectId: SubjectId::fromString(self::SUBJECT_ID), teacherId: UserId::fromString(self::OWNER_TEACHER_ID), title: 'Devoir existant', description: null, dueDate: new DateTimeImmutable('2026-06-15'), status: HomeworkStatus::PUBLISHED, createdAt: $now, updatedAt: $now, ruleOverride: ['warnings' => ['minimum_delay'], 'acknowledgedAt' => '2026-03-18T10:00:00+00:00'], ); /** @var HomeworkRepository $repository */ $repository = static::getContainer()->get(HomeworkRepository::class); $repository->save($homework); } private function persistHomework(string $homeworkId, HomeworkStatus $status): void { $now = new DateTimeImmutable(); $homework = Homework::reconstitute( id: HomeworkId::fromString($homeworkId), tenantId: TenantId::fromString(self::TENANT_ID), classId: ClassId::fromString(self::CLASS_ID), subjectId: SubjectId::fromString(self::SUBJECT_ID), teacherId: UserId::fromString(self::OWNER_TEACHER_ID), title: 'Devoir existant', description: null, dueDate: new DateTimeImmutable('2026-06-15'), status: $status, createdAt: $now, updatedAt: $now, ); /** @var HomeworkRepository $repository */ $repository = static::getContainer()->get(HomeworkRepository::class); $repository->save($homework); } }