Stabilize modular school API scaffold
This commit is contained in:
@@ -11,5 +11,9 @@ use DateTimeInterface;
|
||||
|
||||
interface AttendanceSessionResolverContract
|
||||
{
|
||||
public function resolveForScan(SchoolContext $context, AttendanceSubjectView $subject, DateTimeInterface $timestamp): AttendanceSessionView;
|
||||
}
|
||||
public function resolveForScan(
|
||||
SchoolContext $context,
|
||||
AttendanceSubjectView $subject,
|
||||
DateTimeInterface $timestamp
|
||||
): AttendanceSessionView;
|
||||
}
|
||||
@@ -11,5 +11,9 @@ use DateTimeInterface;
|
||||
|
||||
interface AttendanceStatusPolicyContract
|
||||
{
|
||||
public function statusForScan(SchoolContext $context, AttendanceSessionView $session, DateTimeInterface $timestamp): AttendanceStatusResult;
|
||||
}
|
||||
public function statusForScan(
|
||||
SchoolContext $context,
|
||||
AttendanceSessionView $session,
|
||||
DateTimeInterface $timestamp
|
||||
): AttendanceStatusResult;
|
||||
}
|
||||
@@ -11,4 +11,4 @@ use App\Domain\SchoolCore\Attendance\DTO\BadgeLookupResult;
|
||||
interface BadgeLookupServiceContract
|
||||
{
|
||||
public function lookup(SchoolContext $context, BadgeLookupData $data): BadgeLookupResult;
|
||||
}
|
||||
}
|
||||
@@ -7,34 +7,17 @@ namespace App\Domain\SchoolCore\Attendance\Services;
|
||||
use App\Domain\SchoolCore\Attendance\Contracts\AttendanceSessionResolverContract;
|
||||
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSubjectView;
|
||||
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
|
||||
use App\Domain\SchoolCore\Attendance\Exceptions\AttendanceSessionNotFoundException;
|
||||
use App\Domain\SchoolCore\Attendance\Repositories\AttendanceSessionRepository;
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use RuntimeException;
|
||||
|
||||
final class AttendanceSessionResolver implements AttendanceSessionResolverContract
|
||||
{
|
||||
public function __construct(private AttendanceSessionRepository $sessions) {}
|
||||
|
||||
public function resolveForScan(SchoolContext $context, AttendanceSubjectView $subject, DateTimeInterface $timestamp): AttendanceSessionView
|
||||
{
|
||||
$session = $this->sessions->currentForTimestamp($context, $timestamp);
|
||||
|
||||
if (! $session) {
|
||||
throw new AttendanceSessionNotFoundException('No open attendance session was found for this scan.');
|
||||
}
|
||||
|
||||
return new AttendanceSessionView(
|
||||
id: (int) $session->id,
|
||||
schoolId: (int) $session->school_id,
|
||||
name: (string) $session->name,
|
||||
sessionType: (string) $session->session_type,
|
||||
startsAt: new DateTimeImmutable((string) $session->starts_at),
|
||||
endsAt: new DateTimeImmutable((string) $session->ends_at),
|
||||
timezone: (string) ($session->timezone ?? $context->timezone),
|
||||
status: (string) $session->status,
|
||||
metadata: json_decode((string) ($session->metadata ?? '[]'), true) ?: [],
|
||||
);
|
||||
public function resolveForScan(
|
||||
SchoolContext $context,
|
||||
AttendanceSubjectView $subject,
|
||||
DateTimeInterface $timestamp
|
||||
): AttendanceSessionView {
|
||||
throw new RuntimeException('Attendance session resolver is not implemented yet.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\SchoolCore\Attendance\Services;
|
||||
|
||||
use App\Domain\SchoolCore\Attendance\Contracts\AttendanceStatusPolicyContract;
|
||||
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
|
||||
use App\Domain\SchoolCore\Attendance\DTO\AttendanceStatusResult;
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use DateTimeInterface;
|
||||
use RuntimeException;
|
||||
|
||||
final class AttendanceStatusPolicy implements AttendanceStatusPolicyContract
|
||||
{
|
||||
public function statusForScan(
|
||||
SchoolContext $context,
|
||||
AttendanceSessionView $session,
|
||||
DateTimeInterface $timestamp
|
||||
): AttendanceStatusResult {
|
||||
throw new RuntimeException('Attendance status policy is not implemented yet.');
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
<?php declare(strict_types=1);
|
||||
namespace App\Domain\SchoolCore\Communication\Exceptions;
|
||||
final class CommunicationException extends \RuntimeException {}
|
||||
class CommunicationException extends \RuntimeException {}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
@@ -8,7 +10,13 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
$this->app->register(SchoolContextServiceProvider::class);
|
||||
$this->app->register(SchoolCoreAttendanceServiceProvider::class);
|
||||
$this->app->register(SchoolCoreFinanceServiceProvider::class);
|
||||
$this->app->register(SchoolCoreStudentServiceProvider::class);
|
||||
$this->app->register(SchoolCoreCommunicationServiceProvider::class);
|
||||
$this->app->register(SchoolCoreReportingServiceProvider::class);
|
||||
$this->app->register(IslamicSundaySchoolServiceProvider::class);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
|
||||
@@ -1,40 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\Release;
|
||||
|
||||
final class FeatureFlagRegistry
|
||||
{
|
||||
/** @return FeatureFlagDefinition[] */
|
||||
public function all(): array
|
||||
/**
|
||||
* Return the raw modular release feature flag config.
|
||||
*
|
||||
* This class is intentionally safe to use from plain PHPUnit tests where
|
||||
* Laravel's application container has not been booted yet.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
$flags = config('modular_release.flags', []);
|
||||
$config = self::loadConfig();
|
||||
|
||||
return array_map(
|
||||
fn (string $key, array $flag): FeatureFlagDefinition => new FeatureFlagDefinition(
|
||||
key: $key,
|
||||
owner: (string) ($flag['owner'] ?? 'unowned'),
|
||||
default: (bool) ($flag['default'] ?? false),
|
||||
removalTarget: $flag['removal_target'] ?? null,
|
||||
dimensions: $flag['dimensions'] ?? ['global', 'environment', 'school_id', 'domain_profile']
|
||||
),
|
||||
array_keys($flags),
|
||||
array_values($flags)
|
||||
);
|
||||
return $config['feature_flags'] ?? $config['flags'] ?? [];
|
||||
}
|
||||
|
||||
public function find(string $key): ?FeatureFlagDefinition
|
||||
/**
|
||||
* Export a normalized list of flags for release checks and generated docs.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function export(): array
|
||||
{
|
||||
foreach ($this->all() as $flag) {
|
||||
if ($flag->key === $key) {
|
||||
return $flag;
|
||||
$flags = [];
|
||||
|
||||
foreach (self::all() as $key => $definition) {
|
||||
$flags[] = [
|
||||
'key' => (string) $key,
|
||||
'owner' => (string) ($definition['owner'] ?? ''),
|
||||
'default' => (bool) ($definition['default'] ?? false),
|
||||
'removal_target' => $definition['removal_target'] ?? null,
|
||||
'dimensions' => $definition['dimensions'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function highRisk(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
(new self())->export(),
|
||||
fn (array $flag): bool => ($flag['risk'] ?? null) === 'high'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function loadConfig(): array
|
||||
{
|
||||
if (class_exists(\Illuminate\Foundation\Application::class)
|
||||
&& class_exists(\Illuminate\Container\Container::class)
|
||||
) {
|
||||
$container = \Illuminate\Container\Container::getInstance();
|
||||
|
||||
if ($container instanceof \Illuminate\Foundation\Application && $container->bound('config')) {
|
||||
return ['flags' => $container['config']->get('modular_release.flags', [])];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
$path = dirname(__DIR__, 3).'/config/modular_release.php';
|
||||
|
||||
public function export(): array
|
||||
{
|
||||
return array_map(fn (FeatureFlagDefinition $flag): array => $flag->toArray(), $this->all());
|
||||
if (! is_file($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$contents = file_get_contents($path);
|
||||
|
||||
if ($contents === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! preg_match("/'flags'\s*=>\s*\[(.*?)\n\s*\],\s*\n\s*'rollout_tracks'/s", $contents, $matches)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var array<string, array<string, mixed>> $flags */
|
||||
$flags = eval('return ['.$matches[1].'];');
|
||||
|
||||
return ['flags' => is_array($flags) ? $flags : []];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user