diff --git a/.env.dist b/.env.dist index 7eceb093..d72dd897 100644 --- a/.env.dist +++ b/.env.dist @@ -6,7 +6,6 @@ SEND_GRID_API_KEY=sendgripapikey SEND_GRID_SENDER_EMAIL=phpschool.team@gmail.com REDIS_HOST=redis CACHE.ENABLE=false -CACHE.FPC.ENABLE=false DISPLAY_ERRORS=true GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 9329052b..497639ed 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -33,6 +33,6 @@ jobs: - name: Run phpcs run: composer cs -# -# - name: Run psalm -# run: composer static \ No newline at end of file + + - name: Run PHPStan + run: composer static \ No newline at end of file diff --git a/README.md b/README.md index b75cb332..4bbb4fc3 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,6 @@ docker compose exec php composer app:gen:blog Then navigate to `http://localhost` ! -Pages are cached on first view. -If you need to clear the cache, run `docker compose exec php composer app:cc`. - -You can disable the cache by setting `CACHE.FPC.ENABLE` to `false` in your `.env` file. - ## Build CSS & JS This needs to be done for the main website (non cloud) to run in development mode. diff --git a/app/config.php b/app/config.php index 08469b6f..6404bd1d 100644 --- a/app/config.php +++ b/app/config.php @@ -71,9 +71,7 @@ use PhpSchool\Website\InputFilter\Login as LoginInputFilter; use PhpSchool\Website\InputFilter\SubmitWorkshop as SubmitWorkshopInputFilter; use PhpSchool\Website\InputFilter\WorkshopComposerJson as WorkshopComposerJsonInputFilter; -use PhpSchool\Website\Middleware\FpcCache; use PhpSchool\Website\Middleware\Session as SessionMiddleware; -use PhpSchool\Website\PhpRenderer; use PhpSchool\Website\Repository\DoctrineORMBlogRepository; use PhpSchool\Website\Repository\EventRepository; use PhpSchool\Website\Repository\WorkshopInstallRepository; @@ -122,21 +120,14 @@ 'app' => factory(function (ContainerInterface $c): App { $app = Bridge::create($c); $app->addRoutingMiddleware(); - $app->add($c->get(FpcCache::class)); $app->add(function (Request $request, RequestHandler $handler) use($c) : Response { - $renderer = $this->get(PhpRenderer::class); /** @var Session $session */ $session = $this->get(Session::class); $student = $session->get('student'); $request = $request->withAttribute('student', $student); - $renderer->addAttribute('student', $student); - $renderer->addAttribute( - 'totalExerciseCount', - $c->get(CloudWorkshopRepository::class)->totalExerciseCount() - ); return $handler->handle($request) ->withHeader('cache-control', 'no-cache'); @@ -144,25 +135,8 @@ $app->add(StudentRefresher::class); $app->add(new SessionMiddleware(['name' => 'phpschool'])); - $app->add(function (Request $request, RequestHandler $handler) use ($c){ - $renderer = $c->get(PhpRenderer::class); - $renderer->addAttribute('userAgent', new Agent); - $renderer->addAttribute('route', $request->getUri()->getPath()); - - return $handler->handle($request); - }); - return $app; }), - FpcCache::class => factory(function (ContainerInterface $c): FpcCache { - return new FpcCache($c->get('cache.fpc')); - }), - 'cache.fpc' => factory(function (ContainerInterface $c): CacheInterface { - if (!$c->get('config')['enablePageCache']) { - return new NullAdapter; - } - return new RedisAdapter(new Predis\Client(['host' => $c->get('config')['redisHost']]), 'fpc'); - }), 'cache' => factory(function (ContainerInterface $c): CacheInterface { if (!$c->get('config')['enableCache']) { return new NullAdapter; @@ -183,18 +157,6 @@ return new RedisAdapter($redisConnection, 'default'); }), - PhpRenderer::class => factory(function (ContainerInterface $c): PhpRenderer { - - $settings = $c->get('config')['renderer']; - $renderer = new PhpRenderer( - $settings['template_path'], - [ - 'links' => $c->get('config')['links'], - ], - ); - - return $renderer; - }), LoggerInterface::class => factory(function (ContainerInterface $c): LoggerInterface{ $settings = $c->get('config')['logger']; $logger = new Logger($settings['name']); @@ -215,7 +177,7 @@ //commands ClearCache::class => factory(function (ContainerInterface $c): ClearCache { - return new ClearCache($c->get('cache.fpc')); + return new ClearCache($c->get('cache')); }), CreateAdminUser::class => factory(function (ContainerInterface $c): CreateAdminUser { return new CreateAdminUser($c->get(EntityManagerInterface::class)); @@ -275,14 +237,13 @@ ClearCacheAction::class => function (ContainerInterface $c): ClearCacheAction { return new ClearCacheAction( - $c->get('cache.fpc'), + $c->get('cache'), ); }, Requests::class => \DI\factory(function (ContainerInterface $c): Requests { return new Requests( $c->get(WorkshopRepository::class), - $c->get(PhpRenderer::class) ); }), @@ -290,7 +251,6 @@ return new All( $c->get(WorkshopRepository::class), $c->get(WorkshopInstallRepository::class), - $c->get(PhpRenderer::class) ); }), @@ -298,7 +258,7 @@ return new Approve( $c->get(WorkshopRepository::class), $c->get(WorkshopFeed::class), - $c->get('cache.fpc'), + $c->get('cache'), $c->get(EmailNotifier::class), $c->get(LoggerInterface::class) ); @@ -308,7 +268,7 @@ return new Promote( $c->get(WorkshopRepository::class), $c->get(WorkshopFeed::class), - $c->get('cache.fpc'), + $c->get('cache'), ); }), @@ -317,7 +277,7 @@ $c->get(WorkshopRepository::class), $c->get(WorkshopInstallRepository::class), $c->get(WorkshopFeed::class), - $c->get('cache.fpc'), + $c->get('cache'), ); }), @@ -325,7 +285,6 @@ return new View( $c->get(WorkshopRepository::class), $c->get(WorkshopInstallRepository::class), - $c->get(PhpRenderer::class) ); }, @@ -410,7 +369,7 @@ }, EventAll::class => function (ContainerInterface $c): EventAll { - return new EventAll($c->get(EventRepository::class), $c->get(PhpRenderer::class)); + return new EventAll($c->get(EventRepository::class)); }, EventCreate::class => function (ContainerInterface $c): EventCreate { @@ -424,14 +383,13 @@ return new EventUpdate( $c->get(EventRepository::class), $c->get('form.event'), - $c->get(PhpRenderer::class), ); }, EventDelete::class => function (ContainerInterface $c): EventDelete { return new EventDelete( $c->get(EventRepository::class), - $c->get('cache.fpc'), + $c->get('cache'), ); }, @@ -473,7 +431,6 @@ StudentAuthenticator::class => function (ContainerInterface $c): StudentAuthenticator { return new StudentAuthenticator( $c->get(Session::class), - $c->get(StudentRepository::class) ); }, @@ -528,10 +485,6 @@ public function parse($markdown): string ); }, - Styles::class => function (ContainerInterface $c) { - return new Styles($c->get(PhpRenderer::class)); - }, - CloudWorkshopRepository::class => function (ContainerInterface $c): CloudWorkshopRepository { return new CloudWorkshopRepository($c->get(WorkshopRepository::class)); }, @@ -602,7 +555,6 @@ public function parse($markdown): string 'github-website' => 'https://github.com/php-school/phpschool.io', ], - 'enablePageCache' => filter_var($_ENV['CACHE.FPC.ENABLE'], FILTER_VALIDATE_BOOLEAN), 'enableCache' => filter_var($_ENV['CACHE.ENABLE'], FILTER_VALIDATE_BOOLEAN), 'redisHost' => $_ENV['REDIS_HOST'], 'devMode' => filter_var($_ENV['DEV_MODE'], FILTER_VALIDATE_BOOLEAN), diff --git a/composer.json b/composer.json index 2c04f24f..786123d1 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ "jenssegers/agent": "^2.3", "sendgrid/sendgrid": "^7.9", "vlucas/phpdotenv": "^5.3", - "adamwathan/bootforms": "^0.9", "mnapoli/front-yaml": "^1.5", "laminas/laminas-inputfilter": "^2.28", "laminas/laminas-uri": "^2.11", diff --git a/composer.lock b/composer.lock index 394010cb..7239633d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,102 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5e3adeedbb101c940001acf139b4003c", + "content-hash": "e82b3fe07c060412174d3c6596582201", "packages": [ - { - "name": "adamwathan/bootforms", - "version": "v0.9.0", - "source": { - "type": "git", - "url": "https://github.com/adamwathan/bootforms.git", - "reference": "2d5a8baa79cf6818cfa418276567c6ee6f40ed54" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/adamwathan/bootforms/zipball/2d5a8baa79cf6818cfa418276567c6ee6f40ed54", - "reference": "2d5a8baa79cf6818cfa418276567c6ee6f40ed54", - "shasum": "" - }, - "require": { - "adamwathan/form": "^0.9.0", - "php": ">=5.4.0" - }, - "require-dev": { - "mockery/mockery": "0.9.*", - "phpunit/phpunit": "3.7.*", - "satooshi/php-coveralls": "dev-master" - }, - "type": "library", - "autoload": { - "psr-0": { - "AdamWathan\\BootForms": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Adam Wathan", - "email": "adam.wathan@gmail.com" - } - ], - "description": "Just a Formbuilder with some Bootstrap specific conveniences. Remembers old input, retrieves error messages and handles all your boilerplate Bootstrap markup automatically.", - "support": { - "issues": "https://github.com/adamwathan/bootforms/issues", - "source": "https://github.com/adamwathan/bootforms/tree/master" - }, - "abandoned": true, - "time": "2017-07-18T15:25:51+00:00" - }, - { - "name": "adamwathan/form", - "version": "v0.9.0", - "source": { - "type": "git", - "url": "https://github.com/adamwathan/form.git", - "reference": "d44534bf812512daa9d5a3b68d0ef7928def99b8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/adamwathan/form/zipball/d44534bf812512daa9d5a3b68d0ef7928def99b8", - "reference": "d44534bf812512daa9d5a3b68d0ef7928def99b8", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "illuminate/support": "4.*", - "mockery/mockery": "~0.9", - "phpunit/phpunit": "3.7.*", - "satooshi/php-coveralls": "^1.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "AdamWathan\\Form": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Adam Wathan", - "email": "adam.wathan@gmail.com" - } - ], - "description": "A basic framework agnostic form building package with a few extra niceties like remembering old input and retrieving error messages.", - "support": { - "issues": "https://github.com/adamwathan/form/issues", - "source": "https://github.com/adamwathan/form/tree/master" - }, - "abandoned": true, - "time": "2017-07-07T16:13:45+00:00" - }, { "name": "ahinkle/packagist-latest-version", "version": "2.1.0", diff --git a/public/index.php b/public/index.php index 6f3e407d..056d2df5 100644 --- a/public/index.php +++ b/public/index.php @@ -128,8 +128,7 @@ $group->post('/tour/complete', TourComplete::class); $group->post('/reset', ResetState::class); }) - ->add($container->get(StudentAuthenticator::class)) - ->add(Styles::class); + ->add($container->get(StudentAuthenticator::class)); // Run app $app->run(); diff --git a/src/Action/Admin/Event/Create.php b/src/Action/Admin/Event/Create.php index 6a8db938..d05af5ee 100644 --- a/src/Action/Admin/Event/Create.php +++ b/src/Action/Admin/Event/Create.php @@ -12,13 +12,23 @@ use Psr\Http\Message\ResponseInterface as Response; use Laminas\Filter\Exception\RuntimeException; +/** + * @phpstan-import-type EventData from \PhpSchool\Website\InputFilter\Event + */ class Create { use JsonUtils; private EventRepository $repository; + + /** + * @var FormHandler + */ private FormHandler $formHandler; + /** + * @param FormHandler $formHandler + */ public function __construct( EventRepository $repository, FormHandler $formHandler, @@ -47,11 +57,23 @@ public function __invoke(Request $request, Response $response): MessageInterface ); } + $date = \DateTime::createFromFormat('Y-m-d\TH:i', $values['date']); + + if (false === $date) { + return $this->withJson( + [ + 'success' => false, + 'form_errors' => ['date' => 'Invalid date format'] + ], + $response + ); + } + $event = new Event( $values['name'], $values['description'], $values['link'] ?? null, - \DateTime::createFromFormat('Y-m-d\TH:i', $values['date']), + $date, $values['venue'], isset($values['poster']['tmp_name']) ? basename($values['poster']['tmp_name']) : null ); diff --git a/src/Action/Admin/Event/Update.php b/src/Action/Admin/Event/Update.php index a48a9f9f..e50830ab 100644 --- a/src/Action/Admin/Event/Update.php +++ b/src/Action/Admin/Event/Update.php @@ -4,7 +4,6 @@ use PhpSchool\Website\Action\JsonUtils; use PhpSchool\Website\Form\FormHandler; -use PhpSchool\Website\PhpRenderer; use PhpSchool\Website\Repository\EventRepository; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\ResponseInterface; @@ -12,14 +11,19 @@ use Psr\Http\Message\ResponseInterface as Response; use Laminas\Filter\Exception\RuntimeException; +/** + * @phpstan-import-type EventData from \PhpSchool\Website\InputFilter\Event + */ class Update { use JsonUtils; + /** + * @param FormHandler $formHandler + */ public function __construct( private readonly EventRepository $repository, private readonly FormHandler $formHandler, - private readonly PhpRenderer $renderer, ) { } @@ -55,10 +59,22 @@ public function __invoke(Request $request, Response $response, string $id): Mess ); } + $date = \DateTime::createFromFormat('Y-m-d\TH:i', $values['date']); + + if (false === $date) { + return $this->withJson( + [ + 'success' => false, + 'form_errors' => ['date' => 'Invalid date format'] + ], + $response + ); + } + $event->setName($values['name']) ->setDescription($values['description']) ->setLink($values['link']) - ->setDateTime(\DateTime::createFromFormat('Y-m-d\TH:i', $values['date'])) + ->setDateTime($date) ->setVenue($values['venue']) ->setPoster(isset($values['poster']['tmp_name']) ? basename($values['poster']['tmp_name']) : $event->getPoster()); diff --git a/src/Action/Admin/Login.php b/src/Action/Admin/Login.php index cc3444e8..c8b648d5 100644 --- a/src/Action/Admin/Login.php +++ b/src/Action/Admin/Login.php @@ -5,16 +5,23 @@ use PhpSchool\Website\Action\JsonUtils; use PhpSchool\Website\Form\FormHandler; use PhpSchool\Website\User\AdminAuthenticationService; +use PhpSchool\Website\User\Entity\Admin; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Firebase\JWT\JWT; +/** + * @phpstan-import-type LoginData from \PhpSchool\Website\InputFilter\Login + */ class Login { use JsonUtils; + /** + * @param FormHandler $formHandler + */ public function __construct( private readonly AdminAuthenticationService $authenticationService, private readonly FormHandler $formHandler, @@ -42,6 +49,7 @@ public function __invoke(Request $request, Response $response): MessageInterface ], $response, 401); } + /** @var Admin $admin */ $admin = $result->getIdentity(); $token = [ diff --git a/src/Action/Admin/Workshop/All.php b/src/Action/Admin/Workshop/All.php index e319fcd9..415a10d9 100644 --- a/src/Action/Admin/Workshop/All.php +++ b/src/Action/Admin/Workshop/All.php @@ -8,7 +8,6 @@ use PhpSchool\Website\Action\JsonUtils; use PhpSchool\Website\Entity\Workshop; use PhpSchool\Website\Entity\WorkshopInstall; -use PhpSchool\Website\PhpRenderer; use PhpSchool\Website\Repository\WorkshopInstallRepository; use PhpSchool\Website\Repository\WorkshopRepository; use Psr\Http\Message\ServerRequestInterface as Request; @@ -20,14 +19,11 @@ class All private WorkshopRepository $repository; private WorkshopInstallRepository $workshopInstallRepository; - private PhpRenderer $renderer; public function __construct( WorkshopRepository $repository, WorkshopInstallRepository $workshopInstallRepository, - PhpRenderer $renderer ) { - $this->renderer = $renderer; $this->workshopInstallRepository = $workshopInstallRepository; $this->repository = $repository; } @@ -56,6 +52,9 @@ public function __invoke(Request $request, Response $response): Response ], $response); } + /** + * @return array + */ private function getDateRange(): array { $end = new DateTimeImmutable(); @@ -66,6 +65,11 @@ private function getDateRange(): array return iterator_to_array(new DatePeriod($begin, $interval, $end)); } + /** + * @param array $installs + * @param array $dateRange + * @return array + */ private function getLast30DayInstallGraphData(array $installs, array $dateRange): array { return array_map( diff --git a/src/Action/Admin/Workshop/Approve.php b/src/Action/Admin/Workshop/Approve.php index b20529bc..05932f0b 100644 --- a/src/Action/Admin/Workshop/Approve.php +++ b/src/Action/Admin/Workshop/Approve.php @@ -10,7 +10,6 @@ use Psr\Http\Message\MessageInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; -use PhpSchool\Website\PhpRenderer; use Psr\Log\LoggerInterface; use RuntimeException; @@ -27,7 +26,7 @@ public function __construct( ) { } - public function __invoke(Request $request, Response $response, PhpRenderer $renderer, string $id): MessageInterface + public function __invoke(Request $request, Response $response, string $id): MessageInterface { try { $workshop = $this->repository->findById($id); diff --git a/src/Action/Admin/Workshop/Delete.php b/src/Action/Admin/Workshop/Delete.php index fcff104d..ee6983df 100644 --- a/src/Action/Admin/Workshop/Delete.php +++ b/src/Action/Admin/Workshop/Delete.php @@ -11,7 +11,6 @@ use Psr\Http\Message\MessageInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; -use PhpSchool\Website\PhpRenderer; use RuntimeException; class Delete @@ -27,7 +26,7 @@ public function __construct( ) { } - public function __invoke(Request $request, Response $response, PhpRenderer $renderer, string $id): MessageInterface + public function __invoke(Request $request, Response $response, string $id): MessageInterface { try { $workshop = $this->repository->findById($id); diff --git a/src/Action/Admin/Workshop/Promote.php b/src/Action/Admin/Workshop/Promote.php index fa608135..eb93c9b2 100644 --- a/src/Action/Admin/Workshop/Promote.php +++ b/src/Action/Admin/Workshop/Promote.php @@ -9,7 +9,6 @@ use Psr\Http\Message\MessageInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; -use PhpSchool\Website\PhpRenderer; use RuntimeException; class Promote @@ -23,7 +22,7 @@ public function __construct( ) { } - public function __invoke(Request $request, Response $response, PhpRenderer $renderer, string $id): MessageInterface + public function __invoke(Request $request, Response $response, string $id): MessageInterface { try { $workshop = $this->repository->findById($id); diff --git a/src/Action/Admin/Workshop/Requests.php b/src/Action/Admin/Workshop/Requests.php index 0f266911..716178d7 100644 --- a/src/Action/Admin/Workshop/Requests.php +++ b/src/Action/Admin/Workshop/Requests.php @@ -3,7 +3,6 @@ namespace PhpSchool\Website\Action\Admin\Workshop; use PhpSchool\Website\Action\JsonUtils; -use PhpSchool\Website\PhpRenderer; use PhpSchool\Website\Repository\WorkshopRepository; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; @@ -12,12 +11,10 @@ class Requests { use JsonUtils; - private PhpRenderer $renderer; private WorkshopRepository $repository; - public function __construct(WorkshopRepository $repository, PhpRenderer $renderer) + public function __construct(WorkshopRepository $repository) { - $this->renderer = $renderer; $this->repository = $repository; } diff --git a/src/Action/Admin/Workshop/View.php b/src/Action/Admin/Workshop/View.php index 8be51eb3..34f0c652 100644 --- a/src/Action/Admin/Workshop/View.php +++ b/src/Action/Admin/Workshop/View.php @@ -12,7 +12,6 @@ use Psr\Http\Message\MessageInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; -use PhpSchool\Website\PhpRenderer; use RuntimeException; class View @@ -21,19 +20,16 @@ class View private WorkshopRepository $repository; private WorkshopInstallRepository $workshopInstallRepository; - private PhpRenderer $renderer; public function __construct( WorkshopRepository $repository, WorkshopInstallRepository $workshopInstallRepository, - PhpRenderer $renderer ) { $this->repository = $repository; $this->workshopInstallRepository = $workshopInstallRepository; - $this->renderer = $renderer; } - public function __invoke(Request $request, Response $response, PhpRenderer $renderer, string $id): MessageInterface + public function __invoke(Request $request, Response $response, string $id): MessageInterface { try { $workshop = $this->repository->findById($id); @@ -56,6 +52,10 @@ public function __invoke(Request $request, Response $response, PhpRenderer $rend ], $response); } + /** + * @param array $installs + * @return array{dates: array, data: array} + */ private function getLast30DayInstallGraphData(array $installs): array { $end = new DateTimeImmutable(); diff --git a/src/Action/JsonUtils.php b/src/Action/JsonUtils.php index 683de4e2..375f7ee9 100644 --- a/src/Action/JsonUtils.php +++ b/src/Action/JsonUtils.php @@ -10,18 +10,21 @@ private function jsonSuccess(Response $response): Response { $response ->getBody() - ->write(json_encode(['success' => true])); + ->write((string) json_encode(['success' => true])); return $response ->withStatus(200) ->withHeader('Content-Type', 'application/json'); } + /** + * @param array $json + */ private function withJson(array $json, Response $response, int $status = 200): Response { $response ->getBody() - ->write(json_encode($json)); + ->write((string) json_encode($json)); return $response ->withStatus($status) diff --git a/src/Action/Online/ComposerPackageAdd.php b/src/Action/Online/ComposerPackageAdd.php index 9fdc7d93..e7aa288a 100644 --- a/src/Action/Online/ComposerPackageAdd.php +++ b/src/Action/Online/ComposerPackageAdd.php @@ -5,7 +5,6 @@ use ahinkle\PackagistLatestVersion\PackagistLatestVersion; use GuzzleHttp\Exception\ClientException; use PhpSchool\Website\Action\JsonUtils; -use PhpSchool\Website\PhpRenderer; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -20,7 +19,7 @@ public function __construct(PackagistLatestVersion $packagistLatestVersion) $this->packagistLatestVersion = $packagistLatestVersion; } - public function __invoke(Request $request, Response $response, PhpRenderer $renderer): Response + public function __invoke(Request $request, Response $response): Response { $package = $request->getQueryParams()['package'] ?? null; diff --git a/src/Action/Online/ComposerPackageSearch.php b/src/Action/Online/ComposerPackageSearch.php index 6d249474..ab59faed 100644 --- a/src/Action/Online/ComposerPackageSearch.php +++ b/src/Action/Online/ComposerPackageSearch.php @@ -3,7 +3,6 @@ namespace PhpSchool\Website\Action\Online; use PhpSchool\Website\Action\JsonUtils; -use PhpSchool\Website\PhpRenderer; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -13,7 +12,7 @@ class ComposerPackageSearch private const COMPOSER_PACKAGES_FILE_LOCATION = __DIR__ . '/../../../var/packages.json'; - public function __invoke(Request $request, Response $response, PhpRenderer $renderer): Response + public function __invoke(Request $request, Response $response): Response { $search = $request->getQueryParams()['package'] ?? null; @@ -21,7 +20,8 @@ public function __invoke(Request $request, Response $response, PhpRenderer $rend return $this->withJson(['status' => 'error', 'message' => 'No package set'], $response, 404); } - $packages = json_decode(file_get_contents(self::COMPOSER_PACKAGES_FILE_LOCATION), true); + /** @var null|array{packageNames: array} $packages */ + $packages = json_decode((string) file_get_contents(self::COMPOSER_PACKAGES_FILE_LOCATION), true); $results = []; foreach ($packages['packageNames'] ?? [] as $packageName) { diff --git a/src/Action/Online/RunExercise.php b/src/Action/Online/RunExercise.php index 8c5952a0..7fbeb8f2 100644 --- a/src/Action/Online/RunExercise.php +++ b/src/Action/Online/RunExercise.php @@ -18,10 +18,17 @@ use Psr\Http\Message\ServerRequestInterface as Request; use Symfony\Component\Filesystem\Filesystem; +/** + * @phpstan-type CliRunInfo array{args: array, output: string} + * @phpstan-type CgiRunInfo array{request: array{method: string, uri: string, headers: array>, body: string}, output: string} + */ class RunExercise { use JsonUtils; + /** + * @var array + */ private array $runInfo = []; public function __construct( diff --git a/src/Action/Online/TourComplete.php b/src/Action/Online/TourComplete.php index 6c7aab77..fa098c0d 100644 --- a/src/Action/Online/TourComplete.php +++ b/src/Action/Online/TourComplete.php @@ -3,6 +3,7 @@ namespace PhpSchool\Website\Action\Online; use PhpSchool\Website\Action\JsonUtils; +use PhpSchool\Website\User\Entity\Student; use PhpSchool\Website\User\SessionStorageInterface; use PhpSchool\Website\User\StudentDTO; use PhpSchool\Website\User\StudentRepository; @@ -23,6 +24,7 @@ public function __invoke(Request $request, Response $response): Response { $studentDTO = $this->getStudent(); + /** @var Student $student */ $student = $this->studentRepository->findById($studentDTO->id); $student->setTourComplete(); diff --git a/src/Action/Online/WorkshopExercise.php b/src/Action/Online/WorkshopExercise.php index 5de4e35b..7fb387bd 100644 --- a/src/Action/Online/WorkshopExercise.php +++ b/src/Action/Online/WorkshopExercise.php @@ -7,15 +7,46 @@ use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; use PhpSchool\PhpWorkshop\Solution\SolutionFile; use PhpSchool\Website\Action\JsonUtils; +use PhpSchool\Website\Online\CloudInstalledWorkshop; use PhpSchool\Website\Online\CloudWorkshopRepository; use PhpSchool\Website\Online\ProblemFileConverter; use PhpSchool\Website\Online\StudentWorkshopState; -use PhpSchool\Website\PhpRenderer; use PhpSchool\Website\User\SessionStorageInterface; +use PhpSchool\Website\User\StudentDTO; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; - +use Symfony\Component\String\Slugger\AsciiSlugger; + +/** + * @phpstan-type Exercise array{ + * student: StudentDTO, + * workshop: CloudInstalledWorkshop, + * exercise: array{name: string, slug: string, description: string, type: string}, + * problem: string, + * totalExerciseCount: int, + * } + * + * @phpstan-type ExerciseWithSolution array{ + * student: StudentDTO, + * workshop: CloudInstalledWorkshop, + * exercise: array{name: string, slug: string, description: string, type: string}, + * problem: string, + * totalExerciseCount: int, + * official_solution?: array, + * } + * + * @phpstan-type ExerciseWithInitialFiles array{ + * student: StudentDTO, + * workshop: CloudInstalledWorkshop, + * exercise: array{name: string, slug: string, description: string, type: string}, + * problem: string, + * totalExerciseCount: int, + * official_solution?: array, + * initial_files: array, + * entry_point: string + * } + */ class WorkshopExercise { use JsonUtils; @@ -30,7 +61,6 @@ public function __construct( public function __invoke( Request $request, Response $response, - PhpRenderer $renderer, string $workshop, string $exercise ): MessageInterface { @@ -44,12 +74,14 @@ public function __invoke( ], $response, 404); } + /** @var StudentDTO $student */ + $student = $this->session->get('student'); $data = [ - 'student' => $this->session->get('student'), + 'student' => $student, 'workshop' => $workshop, 'exercise' => [ 'name' => $exercise->getName(), - 'slug' => $renderer->slug($exercise->getName()), + 'slug' => $this->slug($exercise->getName()), 'description' => $exercise->getDescription(), 'type' => $exercise->getType() ], @@ -63,6 +95,10 @@ public function __invoke( return $this->withJson($data, $response); } + /** + * @param Exercise $data + * @return ExerciseWithSolution + */ private function maybeAddOfficialSolution(array $data, ExerciseInterface $exercise): array { if (!$exercise instanceof ProvidesSolution) { @@ -84,6 +120,10 @@ private function maybeAddOfficialSolution(array $data, ExerciseInterface $exerci return $data; } + /** + * @param ExerciseWithSolution $data + * @return ExerciseWithInitialFiles + */ private function addInitialCode(array $data, ExerciseInterface $exercise): array { if (!$exercise instanceof ProvidesInitialCode) { @@ -108,4 +148,9 @@ private function addInitialCode(array $data, ExerciseInterface $exercise): array return $data; } + + private function slug(string $string): string + { + return (new AsciiSlugger())->slug($string)->toString(); + } } diff --git a/src/Action/SlackInvite.php b/src/Action/SlackInvite.php index 3f4b38d8..f27de8dd 100644 --- a/src/Action/SlackInvite.php +++ b/src/Action/SlackInvite.php @@ -16,6 +16,12 @@ public function __construct(private readonly Client $client, private readonly st public function __invoke(Request $request, Response $response): Response { + $data = $request->getParsedBody(); + + if (!is_array($data) && !isset($data['email'])) { + return $this->withJson(['error' => 'Email not set'], $response, 500); + } + try { $apiResponse = $this->client->post('https://phpschool-team.slack.com/api/users.admin.invite', [ 'headers' => [ @@ -23,7 +29,7 @@ public function __invoke(Request $request, Response $response): Response ], 'form_params' => [ 'token' => $this->slackApiToken, - 'email' => $request->getParsedBody()['email'], + 'email' => $data['email'], 'set_active' => true, ], ]); @@ -33,6 +39,10 @@ public function __invoke(Request $request, Response $response): Response $apiResponseData = json_decode($apiResponse->getBody()->__toString(), true); + if (!is_array($apiResponseData)) { + return $this->withJson(['error' => 'An unknown error occurred'], $response, 500); + } + if (isset($apiResponseData['ok']) && $apiResponseData['ok'] === true) { return $this->jsonSuccess($response); } diff --git a/src/Action/StudentLogin.php b/src/Action/StudentLogin.php index 1f3f36f8..f9068555 100644 --- a/src/Action/StudentLogin.php +++ b/src/Action/StudentLogin.php @@ -138,6 +138,11 @@ private function createStudent(GithubResourceOwner $user): Student $this->assertHasValue($email); $this->assertHasValue($name); + /** @var string $id */ + /** @var string $username */ + /** @var string $email */ + /** @var string $name */ + $student = new Student( $id, $username, @@ -175,6 +180,11 @@ private function updateStudent(Student $student, GithubResourceOwner $user): voi $this->assertHasValue($email); $this->assertHasValue($name); + /** @var string $username */ + /** @var string $email */ + /** @var string $name */ + + $student->setUsername($username); $student->setEmail($email); $student->setName($name); diff --git a/src/Action/StudentLogout.php b/src/Action/StudentLogout.php index 3f45119e..d2be441c 100644 --- a/src/Action/StudentLogout.php +++ b/src/Action/StudentLogout.php @@ -13,12 +13,10 @@ class StudentLogout use JsonUtils; private Session $session; - private EntityManagerInterface $entityManager; - public function __construct(Session $session, EntityManagerInterface $entityManager) + public function __construct(Session $session) { $this->session = $session; - $this->entityManager = $entityManager; } public function __invoke(Request $request, Response $response): MessageInterface diff --git a/src/Action/SubmitWorkshop.php b/src/Action/SubmitWorkshop.php index e3ba463d..2785a410 100644 --- a/src/Action/SubmitWorkshop.php +++ b/src/Action/SubmitWorkshop.php @@ -4,7 +4,6 @@ use PhpSchool\Website\Exception\WorkshopCreationException; use PhpSchool\Website\Form\FormHandler; -use PhpSchool\Website\PhpRenderer; use PhpSchool\Website\Service\WorkshopCreator; use PhpSchool\Website\Workshop\EmailNotifier; use Psr\Http\Message\ResponseInterface; @@ -13,15 +12,24 @@ use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; +/** + * @phpstan-import-type SubmitWorkshopData from \PhpSchool\Website\InputFilter\SubmitWorkshop + */ class SubmitWorkshop { use JsonUtils; + /** + * @var FormHandler + */ private FormHandler $formHandler; private WorkshopCreator $workshopCreator; private EmailNotifier $emailNotifier; private LoggerInterface $logger; + /** + * @param FormHandler $formHandler + */ public function __construct( FormHandler $formHandler, WorkshopCreator $workshopCreator, diff --git a/src/Action/TrackDownloads.php b/src/Action/TrackDownloads.php index 203a5642..ecdc5182 100644 --- a/src/Action/TrackDownloads.php +++ b/src/Action/TrackDownloads.php @@ -33,8 +33,11 @@ public function __invoke(Request $request, Response $response, string $workshop, ->withJson(['status' => 'error', 'message' => "Workshop: \"$workshop\" not found."], $response, 404); } + /** @var string $ipAddress */ + $ipAddress = $request->getAttribute('ip_address'); + $this->workshopInstallRepository->save( - new WorkshopInstall($workshopEntity, $request->getAttribute('ip_address'), $version) + new WorkshopInstall($workshopEntity, $ipAddress, $version) ); return $this->jsonSuccess($response); diff --git a/src/Blog/Generator.php b/src/Blog/Generator.php index c612c1cd..01ad24de 100644 --- a/src/Blog/Generator.php +++ b/src/Blog/Generator.php @@ -24,10 +24,14 @@ public function generate(): void collect($this->getMarkDownFiles()) ->map(function (\SplFileInfo $file) { - return $this->parser->parse(file_get_contents($file->getRealPath())); + return $this->parser->parse((string) file_get_contents($file->getRealPath())); }) ->each(function (Document $document) { $meta = $document->getYAML(); + if (!is_array($meta)) { + throw new \RuntimeException('Post meta invalid'); + } + $missing = array_diff_key(array_flip(['date', 'title', 'author', 'author_link']), $meta); if (count($missing) > 0) { @@ -41,10 +45,10 @@ public function generate(): void } }) ->sort(function (Document $documentA, Document $documentB) { - return $documentB->getYAML()['date'] <=> $documentA->getYAML()['date']; + return $documentB->getYAML()['date'] <=> $documentA->getYAML()['date']; //@phpstan-ignore-line }) ->map(function (Document $document) { - return new Post(PostMeta::fromArray($document->getYAML()), $document->getContent()); + return new Post(PostMeta::fromArray($document->getYAML()), $document->getContent()); //@phpstan-ignore-line }) ->each(function (Post $post) { $this->repository->save(BlogPost::fromPost($post)); diff --git a/src/Blog/Post.php b/src/Blog/Post.php index 33659c20..dd7e3852 100644 --- a/src/Blog/Post.php +++ b/src/Blog/Post.php @@ -12,7 +12,7 @@ public function __construct(PostMeta $meta, string $content) $this->meta = $meta; $this->content = $content; - $this->content = preg_replace_callback( + $this->content = (string) preg_replace_callback( '/
(.*?)<\/code><\/pre>/s',
             function ($matches) {
                 return sprintf(
diff --git a/src/Blog/PostMeta.php b/src/Blog/PostMeta.php
index 5de91850..aac65700 100644
--- a/src/Blog/PostMeta.php
+++ b/src/Blog/PostMeta.php
@@ -22,6 +22,9 @@ public function __construct(string $title, DateTime $date, string $author, strin
         $this->link = $this->slugify($this->title);
     }
 
+    /**
+     * @param array{title: string, date: string, author: string, author_link: string} $data
+     */
     public static function fromArray(array $data): self
     {
         return new self($data['title'], new DateTime('@' . $data['date']), $data['author'], $data['author_link']);
@@ -54,9 +57,17 @@ public function getAuthorLink(): string
 
     private function slugify(string $string): string
     {
-        return trim(strtolower(preg_replace('/[^A-Za-z0-9-]+/', '-', $string)), '-');
+        return trim(strtolower((string) preg_replace('/[^A-Za-z0-9-]+/', '-', $string)), '-');
     }
 
+    /**
+     * @return array{
+     *     title: string,
+     *     author: string,
+     *     authorLink: string,
+     *     date: string
+     * }
+     */
     public function jsonSerialize(): array
     {
         return [
diff --git a/src/Command/ClearCache.php b/src/Command/ClearCache.php
index deeaf883..b7d9bcc7 100644
--- a/src/Command/ClearCache.php
+++ b/src/Command/ClearCache.php
@@ -28,6 +28,6 @@ public function __invoke(OutputInterface $output): void
             unlink(__DIR__ . '/../../var/cache/router.php');
         }
 
-        $output->writeln(sprintf('FPC Cleared! Result: %s', $res ? 'true' : 'false'));
+        $output->writeln(sprintf('Cleared! Result: %s', $res ? 'true' : 'false'));
     }
 }
diff --git a/src/Entity/BlogPost.php b/src/Entity/BlogPost.php
index 5d9aafd7..eee5ead8 100644
--- a/src/Entity/BlogPost.php
+++ b/src/Entity/BlogPost.php
@@ -78,6 +78,18 @@ public function getExcerpt(): string
         return substr($content, 0, 200) . "...";
     }
 
+    /**
+     * @return array{
+     *     content: string,
+     *     featuredImage: string,
+     *     excerpt: string,
+     *     title: string,
+     *     slug: string,
+     *     author: string,
+     *     authorLink: string,
+     *     date: string
+     * }
+     */
     public function jsonSerialize(): array
     {
         return [
diff --git a/src/Entity/Event.php b/src/Entity/Event.php
index fbf51ea2..cf866685 100644
--- a/src/Entity/Event.php
+++ b/src/Entity/Event.php
@@ -22,7 +22,7 @@ class Event implements \JsonSerializable
      * @ORM\GeneratedValue(strategy="CUSTOM")
      * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
      */
-    private UuidInterface $id;
+    private UuidInterface $id; /** @phpstan-ignore-line  */
 
     /**
      * @ORM\Column(type="string", length=255)
@@ -130,6 +130,9 @@ public function setVenue(string $venue): Event
         return $this;
     }
 
+    /**
+     * @return array
+     */
     public function getVenueLines(): array
     {
         return explode("\n", $this->venue);
@@ -146,10 +149,23 @@ public function setPoster(?string $poster): Event
         return $this;
     }
 
+    /**
+     * @return array{
+     *     id: string,
+     *     name: string,
+     *     description: string,
+     *     link: ?string,
+     *     date_formatted: string,
+     *     date: string,
+     *     venue: string,
+     *     venueLines: array,
+     *     poster: ?string
+     * }
+     */
     public function toArray(): array
     {
         return [
-            'id' => $this->getId(),
+            'id' => $this->getId()->toString(),
             'name' => $this->getName(),
             'description' => $this->getDescription(),
             'link' => $this->getLink(),
@@ -161,6 +177,19 @@ public function toArray(): array
         ];
     }
 
+    /**
+     * @return array{
+     *     id: string,
+     *     name: string,
+     *     description: string,
+     *     link: ?string,
+     *     date_formatted: string,
+     *     date: string,
+     *     venue: string,
+     *     venueLines: array,
+     *     poster: ?string
+     * }
+     */
     public function jsonSerialize(): array
     {
         return $this->toArray();
diff --git a/src/Entity/Workshop.php b/src/Entity/Workshop.php
index 499c7300..b0d12d8b 100644
--- a/src/Entity/Workshop.php
+++ b/src/Entity/Workshop.php
@@ -20,6 +20,9 @@ class Workshop implements \JsonSerializable
     public const TYPE_COMMUNITY = 0;
     public const TYPE_CORE = 1;
 
+    /**
+     * @var array
+     */
     private array $typeMap = [
         self::TYPE_COMMUNITY => 'community',
         self::TYPE_CORE => 'core',
@@ -33,7 +36,7 @@ class Workshop implements \JsonSerializable
      * @ORM\GeneratedValue(strategy="CUSTOM")
      * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
      */
-    private UuidInterface $id;
+    private UuidInterface $id; /** @phpstan-ignore-line  */
 
     /**
      * @ORM\Column(type="string", length=255)
@@ -92,6 +95,8 @@ class Workshop implements \JsonSerializable
 
     /**
      * @ORM\OneToMany(targetEntity="WorkshopInstall", mappedBy="workshop")
+     *
+     * @var Collection
      */
     private Collection $installs;
 
@@ -194,11 +199,17 @@ public function getType(): int
         return $this->type;
     }
 
+    /**
+     * @return 'core'|'community'
+     */
     public function getTypeCode(): string
     {
         return $this->typeMap[$this->getType()];
     }
 
+    /**
+     * @return 'Core'|'Community'
+     */
     public function getTypeName(): string
     {
         return ucfirst($this->getTypeCode());
@@ -219,6 +230,17 @@ public function getTotalInstalls(): int
         return $this->installs->count();
     }
 
+    /**
+     * @return array{
+     *     workshop_code: string,
+     *     display_name: string,
+     *     github_owner: string,
+     *     github_repo_name: string,
+     *     repo_url: string,
+     *     type: 'core'|'community',
+     *     description: string
+     * }
+     */
     public function toArray(): array
     {
         return [
@@ -232,10 +254,27 @@ public function toArray(): array
         ];
     }
 
+    /**
+     * @return array{
+     *     id: string,
+     *     code: string,
+     *     name: string,
+     *     description: string,
+     *     repo_url: string,
+     *     submitter_name: string,
+     *     submitter_email: string,
+     *     submitter_contact: string|null,
+     *     submitter_avatar: string,
+     *     status: 'Approved'|'Not-approved',
+     *     type: 'Core'|'Community',
+     *     installs: int,
+     *     created_at: string
+     * }
+     */
     public function jsonSerialize(): array
     {
         return [
-            'id' => $this->getId(),
+            'id' => $this->getId()->toString(),
             'code' => $this->getCode(),
             'name' => $this->getDisplayName(),
             'description' => $this->getDescription(),
diff --git a/src/Entity/WorkshopInstall.php b/src/Entity/WorkshopInstall.php
index 734c70af..926517e7 100644
--- a/src/Entity/WorkshopInstall.php
+++ b/src/Entity/WorkshopInstall.php
@@ -22,7 +22,7 @@ class WorkshopInstall
      * @ORM\GeneratedValue(strategy="CUSTOM")
      * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
      */
-    private UuidInterface $id;
+    private UuidInterface $id; /** @phpstan-ignore-line  */
 
     /**
      * @ORM\ManyToOne(targetEntity="Workshop", inversedBy="installs")
diff --git a/src/Exception/WorkshopCreationException.php b/src/Exception/WorkshopCreationException.php
index 3dd0c6d0..dd0231f6 100644
--- a/src/Exception/WorkshopCreationException.php
+++ b/src/Exception/WorkshopCreationException.php
@@ -6,13 +6,16 @@
 
 class WorkshopCreationException extends RuntimeException
 {
-    private array $errors;
-
-    public function __construct(array $errors)
+    /**
+     * @param array|string>> $errors
+     */
+    public function __construct(private array $errors)
     {
-        $this->errors = $errors;
     }
 
+    /**
+     * @return array|string>>
+     */
     public function getErrors(): array
     {
         return $this->errors;
diff --git a/src/Form/ErrorStore.php b/src/Form/ErrorStore.php
deleted file mode 100644
index 61581196..00000000
--- a/src/Form/ErrorStore.php
+++ /dev/null
@@ -1,38 +0,0 @@
-errors = $errors;
-    }
-
-    public function setErrors(array $errors): void
-    {
-        $this->errors = $errors;
-    }
-
-    /**
-     * @param string $key
-     * @return bool
-     */
-    public function hasError($key)
-    {
-        return isset($this->errors[$key]);
-    }
-
-    /**
-     * @param string $key
-     * @return string
-     */
-    public function getError($key)
-    {
-        return array_values($this->errors[$key])[0];
-    }
-}
diff --git a/src/Form/FormHandler.php b/src/Form/FormHandler.php
index 2859351e..2d4890c7 100644
--- a/src/Form/FormHandler.php
+++ b/src/Form/FormHandler.php
@@ -2,10 +2,7 @@
 
 namespace PhpSchool\Website\Form;
 
-use AdamWathan\BootForms\BasicFormBuilder;
-use AdamWathan\BootForms\BootForm;
-use AdamWathan\BootForms\HorizontalFormBuilder;
-use AdamWathan\Form\FormBuilder;
+use Laminas\InputFilter\InputFilter;
 use Laminas\InputFilter\InputFilterInterface;
 use PhpSchool\Website\Action\JsonUtils;
 use PhpSchool\Website\Action\RedirectUtils;
@@ -15,15 +12,24 @@
 use Psr\Http\Message\ResponseInterface as Response;
 use PhpSchool\Website\User\Session;
 
+/**
+ * @template TFilteredValues of array
+ */
 class FormHandler
 {
     use RedirectUtils;
     use JsonUtils;
 
-    private InputFilterInterface $inputFilter;
+    /**
+     * @var InputFilter
+     */
+    private InputFilter $inputFilter;
     private Session $session;
 
-    public function __construct(InputFilterInterface $inputFilter, Session $session)
+    /**
+     * @param InputFilter $inputFilter
+     */
+    public function __construct(InputFilter $inputFilter, Session $session)
     {
         $this->inputFilter = $inputFilter;
         $this->session = $session;
@@ -44,14 +50,6 @@ public function validateAndRedirectIfErrors(Request $request, Response $response
         return $this->redirect($request->getHeaderLine('referer'));
     }
 
-    public function redirectWithErrors(Request $request, Response $response, array $errors): MessageInterface
-    {
-        $this->session->set('__old_input', (array) $request->getParsedBody());
-        $this->session->set('__errors', $errors);
-
-        return $this->redirect($request->getHeaderLine('referer'));
-    }
-
     /**
      * @return bool|MessageInterface
      */
@@ -89,31 +87,9 @@ public function validateRequest(Request $request): bool
         return $this->inputFilter->isValid();
     }
 
-    public function getForm(array $bind = null): BootForm
-    {
-        $formBuilder = new FormBuilder();
-        $formBuilder->setOldInputProvider(new OldInput($this->session->get('__old_input', [])));
-        $formBuilder->setErrorStore(new ErrorStore($this->session->get('__errors', [])));
-
-        if (null !== $bind) {
-            $formBuilder->bind($bind);
-        }
-
-        $this->session->delete('__old_input');
-        $this->session->delete('__errors');
-
-        $basicBootFormsBuilder = new BasicFormBuilder($formBuilder);
-        $horizontalBootFormsBuilder = new HorizontalFormBuilder($formBuilder);
-        return new BootForm($basicBootFormsBuilder, $horizontalBootFormsBuilder);
-    }
-
-    public function getPreviousErrors(): array
-    {
-        $errors = $this->session->get('__errors', []);
-        $this->session->delete('__errors');
-        return $errors;
-    }
-
+    /**
+     * @return TFilteredValues
+     */
     public function getData(): array
     {
         return $this->inputFilter->getValues();
diff --git a/src/Form/FormHandlerFactory.php b/src/Form/FormHandlerFactory.php
index 840459e9..d4d2909b 100644
--- a/src/Form/FormHandlerFactory.php
+++ b/src/Form/FormHandlerFactory.php
@@ -2,6 +2,7 @@
 
 namespace PhpSchool\Website\Form;
 
+use Laminas\InputFilter\InputFilter;
 use PhpSchool\Website\User\Session;
 use Laminas\InputFilter\InputFilterInterface;
 
@@ -14,7 +15,7 @@ public function __construct(Session $session)
         $this->session = $session;
     }
 
-    public function create(InputFilterInterface $inputFilter): FormHandler
+    public function create(InputFilter $inputFilter): FormHandler /** @phpstan-ignore-line  */
     {
         return new FormHandler($inputFilter, $this->session);
     }
diff --git a/src/Form/OldInput.php b/src/Form/OldInput.php
deleted file mode 100644
index 64378985..00000000
--- a/src/Form/OldInput.php
+++ /dev/null
@@ -1,33 +0,0 @@
-oldInput = $oldInput;
-    }
-
-    public function setOldInput(array $oldInput): void
-    {
-        $this->oldInput = $oldInput;
-    }
-
-    public function hasOldInput(): bool
-    {
-        return count($this->oldInput) > 0;
-    }
-
-    /**
-     * @param string $key
-     */
-    public function getOldInput($key): ?string
-    {
-        return $this->oldInput[$key] ?? null;
-    }
-}
diff --git a/src/InputFilter/Event.php b/src/InputFilter/Event.php
index dc1dc689..599b9b28 100644
--- a/src/InputFilter/Event.php
+++ b/src/InputFilter/Event.php
@@ -14,7 +14,15 @@
 use Laminas\Validator\Uri;
 
 /**
- * @psalm-suppress PropertyNotSetInConstructor
+ * @phpstan-type EventData array{
+ *      name: string,
+ *      description: string,
+ *      link: string|null,
+ *      date: string,
+ *      venue: string,
+ *      poster?: array{tmp_name: string},
+ *  }
+ * @extends InputFilter
  */
 class Event extends InputFilter
 {
diff --git a/src/InputFilter/Login.php b/src/InputFilter/Login.php
index f883b165..396218c1 100644
--- a/src/InputFilter/Login.php
+++ b/src/InputFilter/Login.php
@@ -7,7 +7,11 @@
 use Laminas\Validator\StringLength;
 
 /**
- * @psalm-suppress PropertyNotSetInConstructor
+ * @phpstan-type LoginData array{
+ *      email: string,
+ *      password: string,
+ *  }
+ * @extends InputFilter
  */
 class Login extends InputFilter
 {
diff --git a/src/InputFilter/SubmitWorkshop.php b/src/InputFilter/SubmitWorkshop.php
index 20ebeec8..38e11075 100644
--- a/src/InputFilter/SubmitWorkshop.php
+++ b/src/InputFilter/SubmitWorkshop.php
@@ -12,7 +12,14 @@
 use Laminas\Validator\StringLength;
 
 /**
- * @psalm-suppress PropertyNotSetInConstructor
+ * @phpstan-type SubmitWorkshopData array{
+ *      github-url: string,
+ *      email: string,
+ *      name: string,
+ *      contact?: string,
+ *      workshop-name: string,
+ *  }
+ * @extends InputFilter
  */
 class SubmitWorkshop extends InputFilter
 {
@@ -28,7 +35,7 @@ public function __construct(Client $gitHubClient, WorkshopRepository $workshopRe
                 [
                     'name' => Regex::class,
                     'options' => [
-                        'pattern' => static::$gitHubRepoUrlRegex,
+                        'pattern' => self::$gitHubRepoUrlRegex,
                         'messages' => [
                             Regex::NOT_MATCH => 'The URL "%value%" is not a valid GitHub repository URL.',
                         ],
@@ -39,13 +46,13 @@ public function __construct(Client $gitHubClient, WorkshopRepository $workshopRe
                     'name' => Callback::class,
                     'options' => [
                         'callback' => function (string $url) {
-                            preg_match(static::$gitHubRepoUrlRegex, $url, $matches);
+                            preg_match(self::$gitHubRepoUrlRegex, $url, $matches);
                             $owner = $matches[3];
                             $repo = $matches[4];
 
                             try {
                                 $response = (new \GuzzleHttp\Client())
-                                    ->request('GET', sprintf(static::$gitHubComposerJsonUrlFormat, $owner, $repo));
+                                    ->request('GET', sprintf(self::$gitHubComposerJsonUrlFormat, $owner, $repo));
                                 return $response->getStatusCode() === 200;
                             } catch (TransferException $e) {
                                 return false;
@@ -61,7 +68,7 @@ public function __construct(Client $gitHubClient, WorkshopRepository $workshopRe
                     'name' => Callback::class,
                     'options' => [
                         'callback' => function (string $url) use ($gitHubClient) {
-                            preg_match(static::$gitHubRepoUrlRegex, $url, $matches);
+                            preg_match(self::$gitHubRepoUrlRegex, $url, $matches);
                             $owner = $matches[3];
                             $repository = $matches[4];
 
diff --git a/src/InputFilter/WorkshopComposerJson.php b/src/InputFilter/WorkshopComposerJson.php
index c4175174..da5bbe31 100644
--- a/src/InputFilter/WorkshopComposerJson.php
+++ b/src/InputFilter/WorkshopComposerJson.php
@@ -9,7 +9,12 @@
 use Laminas\Validator\StringLength;
 
 /**
- * @psalm-suppress PropertyNotSetInConstructor
+ * @phpstan-type WorkshopComposerJsonData array{
+ *      name: string,
+ *      bin: string,
+ *      description: string
+ * }
+ * @extends InputFilter
  */
 class WorkshopComposerJson extends InputFilter
 {
diff --git a/src/Middleware/AdminStyle.php b/src/Middleware/AdminStyle.php
deleted file mode 100644
index eb7236d6..00000000
--- a/src/Middleware/AdminStyle.php
+++ /dev/null
@@ -1,30 +0,0 @@
-renderer = $renderer;
-        $this->devMode = $devMode;
-    }
-
-    public function __invoke(Request $request, RequestHandler $handler): Response
-    {
-        $this->renderer
-            ->removeCss('code-blocks')
-            ->removeJs('online')
-            ->removeJs('highlight-js');
-
-        return $handler->handle($request);
-    }
-}
diff --git a/src/Middleware/FpcCache.php b/src/Middleware/FpcCache.php
deleted file mode 100644
index a9df41b7..00000000
--- a/src/Middleware/FpcCache.php
+++ /dev/null
@@ -1,99 +0,0 @@
-cache = $cache;
-    }
-
-    public function __invoke(Request $request, RequestHandler $handler): Response
-    {
-        $key = sprintf('fpc-route-%s-%s', $this->encodeUrl($request->getUri()->getPath()), $request->getMethod());
-
-        $item = $this->cache->getItem($key);
-
-        if ($item->isHit()) {
-            return $this->unserialize($item->get());
-        }
-
-        $response = $handler->handle($request);
-
-        if ($this->canSave($request, $response)) {
-            $item->set($this->serialize($response));
-            $item->expiresAt(new \DateTime('now + 1 month'));
-            $this->cache->save($item);
-        }
-
-        return $response;
-    }
-
-    /**
-     * This is not robust enough
-     */
-    private function encodeUrl(string $urlPath): string
-    {
-        return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $urlPath)));
-    }
-
-    private function serialize(Response $response): string
-    {
-        return json_encode([
-            'body'      => (string) $response->getBody(),
-            'headers'   => $response->getHeaders(),
-        ]);
-    }
-
-    private function unserialize(string $responseData): Response
-    {
-        $responseData = json_decode($responseData, true);
-        $response = new GuzzleResponse(200, $responseData['headers']);
-        $response->getBody()->write($responseData['body']);
-        return $response;
-    }
-
-    /**
-     * Check whether the response can be saved or not.
-     */
-    private function canSave(Request $request, Response $response): bool
-    {
-        if ($request->getMethod() !== 'GET') {
-            return false;
-        }
-
-        if ($response->getStatusCode() !== 200) {
-            return false;
-        }
-
-        if ($this->hasNoCacheHeader($response)) {
-            return false;
-        }
-
-        return true;
-    }
-
-    private function hasNoCacheHeader(Response $response): bool
-    {
-        $cacheControl = $response->getHeaderLine('Cache-Control');
-
-        if (!$cacheControl) {
-            return false;
-        }
-
-        if ((stripos($cacheControl, 'no-cache') !== false || stripos($cacheControl, 'no-store') !== false)) {
-            return true;
-        }
-
-        return false;
-    }
-}
diff --git a/src/Middleware/Session.php b/src/Middleware/Session.php
index 1c77ad12..722be7b6 100644
--- a/src/Middleware/Session.php
+++ b/src/Middleware/Session.php
@@ -8,6 +8,17 @@
 
 class Session
 {
+    /**
+     * @var array{
+     *       name: string,
+     *       lifetime: int,
+     *       path: string|null,
+     *       domain: string|null,
+     *       secure: bool,
+     *       httponly: bool,
+     *       cache_limiter: string
+     *   }
+     */
     private array $options = [
         'name' => 'phpschool',
         'lifetime' => 0, //until the browser is closed
@@ -18,13 +29,45 @@ class Session
         'cache_limiter' => 'nocache',
     ];
 
+    /**
+     * @param array{
+     *       name?: string,
+     *       lifetime?: int,
+     *       path?: string|null,
+     *       domain?: string|null,
+     *       secure?: bool,
+     *       httponly?: bool,
+     *       cache_limiter?: string
+     *   } $options
+     */
     public function __construct(array $options = [])
     {
-        $keys = array_keys($this->options);
-        foreach ($keys as $key) {
-            if (array_key_exists($key, $options)) {
-                $this->options[$key] = $options[$key];
-            }
+        if (isset($options['name'])) {
+            $this->options['name'] = $options['name'];
+        }
+
+        if (isset($options['lifetime'])) {
+            $this->options['lifetime'] = $options['lifetime'];
+        }
+
+        if (isset($options['path'])) {
+            $this->options['path'] = $options['path'];
+        }
+
+        if (isset($options['domain'])) {
+            $this->options['domain'] = $options['domain'];
+        }
+
+        if (isset($options['secure'])) {
+            $this->options['secure'] = $options['secure'];
+        }
+
+        if (isset($options['httponly'])) {
+            $this->options['httponly'] = $options['httponly'];
+        }
+
+        if (isset($options['cache_limiter'])) {
+            $this->options['cache_limiter'] = $options['cache_limiter'];
         }
     }
 
diff --git a/src/Online/CloudInstalledWorkshop.php b/src/Online/CloudInstalledWorkshop.php
index 53cde3d2..272683ad 100644
--- a/src/Online/CloudInstalledWorkshop.php
+++ b/src/Online/CloudInstalledWorkshop.php
@@ -19,14 +19,12 @@
 
 class CloudInstalledWorkshop implements \JsonSerializable
 {
-    private Application $application;
     private \DI\Container $container;
     private Workshop $workshop;
     private AsciiSlugger $slugger;
 
     public function __construct(Application $application, Workshop $workshop)
     {
-        $this->application = $application;
         /** @var \DI\Container $container */
         $container = $application->configure();
         $this->container = $container;
@@ -52,6 +50,9 @@ public function getDescription(): string
         return $this->workshop->getDescription();
     }
 
+    /**
+     * @return 'core'|'community'
+     */
     public function getType(): string
     {
         return $this->workshop->getTypeCode();
@@ -62,7 +63,9 @@ public function getType(): string
      */
     public function findAllExercises(): array
     {
-        return $this->container->get(ExerciseRepository::class)->findAll();
+        /** @var ExerciseRepository $repo */
+        $repo = $this->container->get(ExerciseRepository::class);
+        return $repo->findAll();
     }
 
     public function findExerciseBySlug(string $slug): ExerciseInterface
@@ -101,7 +104,9 @@ public function findNextExercise(ExerciseInterface $currentExercise): ?ExerciseI
 
     public function getExerciseDispatcher(): ExerciseDispatcher
     {
-        return $this->container->get(ExerciseDispatcher::class);
+        /** @var ExerciseDispatcher $exerciseDispatcher */
+        $exerciseDispatcher = $this->container->get(ExerciseDispatcher::class);
+        return $exerciseDispatcher;
     }
 
     public function getService(string $service): mixed
@@ -122,7 +127,9 @@ private function configureEvents(): void
         /** @var EventDispatcher $eventDispatch */
         $eventDispatch = $this->container->get(EventDispatcher::class);
 
+        /** @var InitialCodeListener $initialCodeListener */
         $initialCodeListener = $this->container->get(InitialCodeListener::class);
+        /** @var OutputRunInfoListener $outputRunInfoListener */
         $outputRunInfoListener = $this->container->get(OutputRunInfoListener::class);
 
         $eventDispatch->removeListener('exercise.selected', [$initialCodeListener, '__invoke']);
@@ -130,12 +137,30 @@ private function configureEvents(): void
         $eventDispatch->removeListener('cgi.run.student-execute.pre', [$outputRunInfoListener, '__invoke']);
     }
 
+    /**
+     * @return array{
+     *     name: string,
+     *     code: string,
+     *     logo: string,
+     *     description: string,
+     *     type: 'core'|'community',
+     *     exercises: array
+     * }
+     */
     public function jsonSerialize(): array
     {
+        /** @var string $logo */
+        $logo = $this->getService('workshopLogo');
+
         return [
             'name' => $this->getName(),
             'code' => $this->getCode(),
-            'logo' => $this->getService('workshopLogo'),
+            'logo' => $logo,
             'description' => $this->getDescription(),
             'type' => $this->getType(),
             'exercises' => array_map(function (ExerciseInterface $exercise) {
diff --git a/src/Online/CloudWorkshopRepository.php b/src/Online/CloudWorkshopRepository.php
index 393a820b..76139862 100644
--- a/src/Online/CloudWorkshopRepository.php
+++ b/src/Online/CloudWorkshopRepository.php
@@ -37,7 +37,9 @@ public function findAll(): array
             function (string $packageName) {
                 /** @var string $path */
                 $path = InstalledVersions::getInstallPath($packageName);
+                /** @var string $path */
                 $path = realpath($path);
+
                 $workshop = $this->workshopRepository->findByCode(
                     $this->getWorkshopCode($path)
                 );
@@ -53,9 +55,12 @@ function (string $packageName) {
 
     public function totalExerciseCount(): int
     {
-        return collect($this->findAll())
+        /** @var int $total */
+        $total = collect($this->findAll())
             ->map(fn (CloudInstalledWorkshop $worksop) => count($worksop->findAllExercises()))
             ->sum();
+
+        return $total;
     }
 
     /**
@@ -64,10 +69,14 @@ public function totalExerciseCount(): int
      */
     private function getWorkshopCode(string $path): string
     {
-        $json = json_decode(file_get_contents($path . '/composer.json'), true);
+        /** @var array{bin: non-empty-list} $json */
+        $json = json_decode((string) file_get_contents($path . '/composer.json'), true);
         return basename($json['bin'][0]);
     }
 
+    /**
+     * @return list
+     */
     private function getComposerInstalledWorkshops(): array
     {
         return array_unique(InstalledVersions::getInstalledPackagesByType('php-school-workshop'));
diff --git a/src/Online/Middleware/ExerciseRunnerRateLimiter.php b/src/Online/Middleware/ExerciseRunnerRateLimiter.php
index 51f768af..05dcd735 100644
--- a/src/Online/Middleware/ExerciseRunnerRateLimiter.php
+++ b/src/Online/Middleware/ExerciseRunnerRateLimiter.php
@@ -3,6 +3,7 @@
 namespace PhpSchool\Website\Online\Middleware;
 
 use PhpSchool\Website\User\SessionStorageInterface;
+use PhpSchool\Website\User\StudentDTO;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
 use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
@@ -21,9 +22,10 @@ public function __construct(SessionStorageInterface $session, RateLimiterFactory
 
     public function __invoke(Request $request, RequestHandler $handler): Response
     {
-        $limiter = $this->limiterFactory->create(
-            $this->session->get('student')->id->toString()
-        );
+        /** @var StudentDTO $student */
+        $student = $this->session->get('student');
+
+        $limiter = $this->limiterFactory->create($student->id->toString());
 
         $limit = $limiter->consume();
 
diff --git a/src/Online/Middleware/Styles.php b/src/Online/Middleware/Styles.php
deleted file mode 100644
index 79d05880..00000000
--- a/src/Online/Middleware/Styles.php
+++ /dev/null
@@ -1,28 +0,0 @@
-renderer = $renderer;
-    }
-
-    public function __invoke(Request $request, RequestHandler $handler): Response
-    {
-        $this->renderer
-            ->removeCss('code-blocks')
-            ->removeJs('jquery')
-            ->removeJs('main-js');
-
-        return $handler->handle($request);
-    }
-}
diff --git a/src/Online/ProblemFileConverter.php b/src/Online/ProblemFileConverter.php
index 5385ae2a..d015c219 100644
--- a/src/Online/ProblemFileConverter.php
+++ b/src/Online/ProblemFileConverter.php
@@ -22,6 +22,6 @@ public function htmlFromExercise(ExerciseInterface $exercise): string
             throw ProblemFileDoesNotExistException::fromFile($problemFile);
         }
 
-        return $this->commonMarkConverter->convertToHtml(file_get_contents($problemFile));
+        return $this->commonMarkConverter->convertToHtml((string) file_get_contents($problemFile));
     }
 }
diff --git a/src/Online/ProjectUploader.php b/src/Online/ProjectUploader.php
index 789b86c7..6802601d 100644
--- a/src/Online/ProjectUploader.php
+++ b/src/Online/ProjectUploader.php
@@ -19,7 +19,7 @@ public function upload(Request $request, StudentDTO $student): DirectorySolution
     {
         $data = json_decode($request->getBody()->__toString(), true);
 
-        if (empty($data['scripts'])) {
+        if (!is_array($data) || !isset($data['scripts']) || !is_array($data['scripts']) || count($data['scripts']) < 1) {
             throw new \RuntimeException('No files were uploaded');
         }
 
@@ -57,6 +57,9 @@ public function upload(Request $request, StudentDTO $student): DirectorySolution
         return new DirectorySolution($basePath, $entryPoint, ['composer.lock', 'vendor']);
     }
 
+    /**
+     * @param array $scripts
+     */
     private function writeScripts(array $scripts, string $basePath): void
     {
         foreach ($scripts as $filePath => $content) {
diff --git a/src/Online/StudentCloudState.php b/src/Online/StudentCloudState.php
index 93045c8b..9435fd7e 100644
--- a/src/Online/StudentCloudState.php
+++ b/src/Online/StudentCloudState.php
@@ -4,10 +4,20 @@
 
 use PhpSchool\PhpWorkshop\UserState\UserState;
 
+/**
+ * @phpstan-type WorkshopState array, currentExercise: string|null}>
+ * @phpstan-type StudentState array{}|array{workshops: WorkshopState, total_completed: int}
+ */
 class StudentCloudState implements \JsonSerializable
 {
+    /**
+     * @var WorkshopState
+     */
     private array $workshopState;
 
+    /**
+     * @param WorkshopState $workshopState
+     */
     public function __construct(array $workshopState)
     {
         $this->workshopState = $workshopState;
@@ -26,12 +36,21 @@ public function getStateForWorkshop(string $workshop): UserState
 
     public function getTotalCompletedExercises(): int
     {
-        return collect($this->workshopState)
+        /** @var int $total */
+        $total = collect($this->workshopState)
             ->pluck('completedExercises')
             ->map(fn (array $completed) => count($completed))
             ->sum();
+
+        return $total;
     }
 
+    /**
+     * @return array{
+     *     workshops: WorkshopState,
+     *     total_completed: int
+     * }
+     */
     public function jsonSerialize(): array
     {
         return [
diff --git a/src/Online/VueResultsRenderer.php b/src/Online/VueResultsRenderer.php
index 7e7f891c..ca248a79 100644
--- a/src/Online/VueResultsRenderer.php
+++ b/src/Online/VueResultsRenderer.php
@@ -11,8 +11,24 @@
 use PhpSchool\PhpWorkshop\Result\SuccessInterface;
 use PhpSchool\PhpWorkshop\ResultAggregator;
 
+/**
+ * @phpstan-type InnerResult array{
+ *      success: bool,
+ *      name: string,
+ *      type?: class-string
+ * }
+ * @phpstan-type Result array{
+ *     success: bool,
+ *     name: string,
+ *     type?: class-string,
+ *     results?: array
+ * }
+ */
 class VueResultsRenderer
 {
+    /**
+     * @return array
+     */
     public function render(
         ResultAggregator $results,
         ExerciseInterface $exercise
@@ -36,6 +52,9 @@ private function isResultGroup(ResultInterface $result): bool
         return $result instanceof ResultGroupInterface;
     }
 
+    /**
+     * @return Result
+     */
     private function renderResult(ResultInterface $result): array
     {
         if ($this->isSuccess($result)) {
@@ -47,38 +66,41 @@ private function renderResult(ResultInterface $result): array
 
         if ($this->isResultGroup($result)) {
             /** @var ResultGroupInterface $result */
+            /** @var array $results */
+            $results = array_map(
+                function (ResultInterface $innerResult) use ($result) {
+
+                    if ($result->isResultSuccess($innerResult)) {
+                        return [
+                            'success' => true,
+                            'name' => $innerResult->getCheckName(),
+                        ];
+                    }
+
+                    /** @var FailureInterface $innerResult  */
+                    return array_merge(
+                        [
+                            'success' => false,
+                            'name' => $innerResult->getCheckName(),
+                            'type' => $innerResult::class,
+                        ],
+                        $innerResult->toArray()
+                    );
+                },
+                $result->getResults()
+            );
+
             return [
                 'success' => false,
                 'name' => $result->getCheckName(),
                 'type' => $result::class,
-                'results' => array_map(
-                    function (ResultInterface $innerResult) use ($result) {
-
-                        if ($result->isResultSuccess($innerResult)) {
-                            return [
-                                'success' => true,
-                                'name' => $innerResult->getCheckName(),
-                            ];
-                        }
-
-                        /** @var FailureInterface $innerResult  */
-
-                        return array_merge(
-                            [
-                                'success' => false,
-                                'name' => $innerResult->getCheckName(),
-                                'type' => $innerResult::class,
-                            ],
-                            $innerResult->toArray()
-                        );
-                    },
-                    $result->getResults()
-                )
+                'results' => $results
             ];
         }
 
         /** @var FailureInterface $result */
-        return array_merge(
+        /** @var InnerResult $failure */
+        $failure = array_merge(
             [
                 'success' => false,
                 'name' => $result->getCheckName(),
@@ -86,5 +108,7 @@ function (ResultInterface $innerResult) use ($result) {
             ],
             $result->toArray()
         );
+
+        return $failure;
     }
 }
diff --git a/src/PhpRenderer.php b/src/PhpRenderer.php
deleted file mode 100644
index 288d2e70..00000000
--- a/src/PhpRenderer.php
+++ /dev/null
@@ -1,207 +0,0 @@
-
-     */
-    private array $css = [];
-
-    /**
-     * @var list
-     */
-    private array $js = [];
-
-    /**
-     * @var list
-     */
-    private array $preload = [];
-
-    private static int $jsonFlags = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
-
-    public function __construct(string $templatePath = '', array $attributes = [])
-    {
-        $this->templatePath = rtrim($templatePath, '/\\') . '/';
-        $this->attributes = $attributes;
-    }
-
-    public function addAttribute(string $key, mixed $value): void
-    {
-        $this->attributes[$key] = $value;
-    }
-
-    public function getAttribute(string $key): mixed
-    {
-        if (!isset($this->attributes[$key])) {
-            return false;
-        }
-
-        return $this->attributes[$key];
-    }
-
-    public function prependLocalCss(string $id, string $cssFile): PhpRenderer
-    {
-        array_unshift($this->css, ['id' => $id, 'url' => $cssFile, 'type' => 'local']);
-        return $this;
-    }
-
-    public function appendLocalCss(string $id, string $cssFile): PhpRenderer
-    {
-        $this->css[] = ['id' => $id, 'url' => $cssFile, 'type' => 'local'];
-        return $this;
-    }
-
-    public function prependRemoteCss(string $id, string $cssFile): PhpRenderer
-    {
-        array_unshift($this->css, ['id' => $id, 'url' => $cssFile, 'type' => 'remote']);
-        return $this;
-    }
-
-    public function appendRemoteCss(string $id, string $cssFile): PhpRenderer
-    {
-        $this->css[] = ['id' => $id, 'url' => $cssFile, 'type' => 'remote'];
-        return $this;
-    }
-
-    public function removeCss(string $id): PhpRenderer
-    {
-        $this->css = array_values(array_filter($this->css, function (array $css) use ($id) {
-            return $css['id'] !== $id;
-        }));
-        return $this;
-    }
-
-    public function renderCss(): string
-    {
-        return implode("\n", array_map(function (array $css) {
-            if ($css['type'] === 'local') {
-                return sprintf('', file_get_contents($css['url']));
-            } else {
-                return sprintf('', $css['url']);
-            }
-        }, $this->css));
-    }
-
-    public function addJs(string $id, string $jsFile, array $tags = ['defer']): PhpRenderer
-    {
-        $this->js[] = ['id' => $id, 'url' => $jsFile, 'tags' => $tags];
-        return $this;
-    }
-
-    public function removeJs(string $id): PhpRenderer
-    {
-        $this->js = array_values(array_filter($this->js, function (array $js) use ($id) {
-            return $js['id'] !== $id;
-        }));
-        return $this;
-    }
-
-    public function getJs(): array
-    {
-        return collect($this->js)
-            ->map(function (array $js) {
-                $tags = collect($js['tags'] ?? [])
-                    ->map(function (string $value, string|int $key) {
-                        return is_int($key) ? $value : sprintf('%s="%s"', $key, $value);
-                    })
-                    ->implode(' ');
-
-                return ['src' => $js['url'], 'tags' => $tags];
-            })
-            ->toArray();
-    }
-
-    public function addPreload(string $id, string $file): PhpRenderer
-    {
-        $this->preload[] = ['id' => $id, 'url' => $file];
-        return $this;
-    }
-
-    public function getPreload(): array
-    {
-        return array_map(fn ($preload) => $preload['url'], $this->preload);
-    }
-
-    public function removePreload(string $id): PhpRenderer
-    {
-        $this->css = array_values(array_filter($this->preload, function (array $preload) use ($id) {
-            return $preload['id'] !== $id;
-        }));
-        return $this;
-    }
-
-    public function slugClass(string $class): string
-    {
-        return str_replace('\\', '-', strtolower($class));
-    }
-
-    public function render(ResponseInterface $response, string $template, array $data = []): ResponseInterface
-    {
-        $output = $this->fetch($template, $data, true);
-        $response->getBody()->write($output);
-        return $response;
-    }
-
-    public function fetch(string $template, array $data = [], bool $useLayout = false): string
-    {
-        return $this->fetchTemplate($template, $data);
-    }
-
-    public function fetchTemplate(string $template, array $data = []): string
-    {
-        if (isset($data['template'])) {
-            throw new \InvalidArgumentException('Duplicate template key found');
-        }
-
-        if (!$this->templateExists($template)) {
-            throw new \InvalidArgumentException('View cannot render "' . $template
-                . '" because the template does not exist');
-        }
-
-        $data = array_merge($this->attributes, $data);
-        try {
-            ob_start();
-            $this->protectedIncludeScope($this->templatePath . $template, $data);
-            $output = ob_get_clean();
-        } catch (\Throwable $e) {
-            ob_end_clean();
-            throw $e;
-        }
-
-        return $output;
-    }
-
-    public function templateExists(string $template): bool
-    {
-        $path = $this->templatePath . $template;
-        return is_file($path) && is_readable($path);
-    }
-
-    /**
-     * @psalm-suppress UnresolvableInclude
-     */
-    private function protectedIncludeScope(string $template, array $data): void
-    {
-        extract($data);
-        include func_get_arg(0);
-    }
-
-    public function slug(string $string): string
-    {
-        return (new AsciiSlugger())->slug($string)->toString();
-    }
-
-    public function json(mixed $var): string
-    {
-        return json_encode($var, self::$jsonFlags);
-    }
-}
diff --git a/src/Repository/DoctrineORMBlogRepository.php b/src/Repository/DoctrineORMBlogRepository.php
index 8e0de9b7..473a47ed 100644
--- a/src/Repository/DoctrineORMBlogRepository.php
+++ b/src/Repository/DoctrineORMBlogRepository.php
@@ -18,10 +18,13 @@ class DoctrineORMBlogRepository extends EntityRepository
      */
     public function findAll(): array
     {
-        return $this->createQueryBuilder('e')
+        /** @var array $result */
+        $result = $this->createQueryBuilder('e')
             ->orderBy('e.dateTime', 'DESC')
             ->getQuery()
             ->getResult();
+
+        return $result;
     }
 
     public function save(BlogPost $post): void
diff --git a/src/Repository/DoctrineORMEventRepository.php b/src/Repository/DoctrineORMEventRepository.php
index e91c0969..a7c10a1d 100644
--- a/src/Repository/DoctrineORMEventRepository.php
+++ b/src/Repository/DoctrineORMEventRepository.php
@@ -17,13 +17,16 @@ class DoctrineORMEventRepository extends EntityRepository implements EventReposi
      */
     public function findPrevious(int $limit = 10): array
     {
-        return $this->createQueryBuilder('e')
+        /** @var list $result */
+        $result = $this->createQueryBuilder('e')
             ->where('e.dateTime <= :now')
             ->orderBy('e.dateTime', 'DESC')
             ->setMaxResults($limit)
             ->setParameter(':now', new \DateTime())
             ->getQuery()
             ->getResult();
+
+        return $result;
     }
 
     /**
@@ -31,13 +34,16 @@ public function findPrevious(int $limit = 10): array
      */
     public function findUpcoming(int $limit = 10): array
     {
-        return $this->createQueryBuilder('e')
+        /** @var list $result */
+        $result = $this->createQueryBuilder('e')
             ->where('e.dateTime > :now')
             ->orderBy('e.dateTime', 'ASC')
             ->setMaxResults($limit)
             ->setParameter(':now', new \DateTime())
             ->getQuery()
             ->getResult();
+
+        return $result;
     }
 
     /**
diff --git a/src/Repository/DoctrineORMWorkshopInstallRepository.php b/src/Repository/DoctrineORMWorkshopInstallRepository.php
index 467ad67c..98258925 100644
--- a/src/Repository/DoctrineORMWorkshopInstallRepository.php
+++ b/src/Repository/DoctrineORMWorkshopInstallRepository.php
@@ -7,6 +7,9 @@
 use PhpSchool\Website\Entity\Workshop;
 use PhpSchool\Website\Entity\WorkshopInstall;
 
+/**
+ * @extends EntityRepository
+ */
 class DoctrineORMWorkshopInstallRepository extends EntityRepository implements WorkshopInstallRepository
 {
     public function totalInstallsInLast30Days(Workshop $workshop): int
@@ -31,7 +34,10 @@ public function totalInstallsInLast30Days(Workshop $workshop): int
                 'workshop' => $workshop
             ]);
 
-        return $qb->getQuery()->getSingleScalarResult() ?? 0;
+        /** @var ?int $result */
+        $result = $qb->getQuery()->getSingleScalarResult();
+
+        return  $result ?? 0;
     }
 
     public function totalInstalls(Workshop $workshop): int
@@ -41,7 +47,10 @@ public function totalInstalls(Workshop $workshop): int
             ->where('s.workshop = :workshop')
             ->setParameter('workshop', $workshop);
 
-        return $qb->getQuery()->getSingleScalarResult() ?? 0;
+        /** @var ?int $result */
+        $result = $qb->getQuery()->getSingleScalarResult();
+
+        return  $result ?? 0;
     }
 
     /**
@@ -67,7 +76,9 @@ public function findInstallsInLast30Days(Workshop $workshop): array
                 'workshop' => $workshop
             ]);
 
-        return $qb->getQuery()->getResult();
+        /** @var array $result */
+        $result = $qb->getQuery()->getResult();
+        return $result;
     }
 
     public function save(WorkshopInstall $workshopInstall): void
diff --git a/src/Repository/WorkshopInstallRepository.php b/src/Repository/WorkshopInstallRepository.php
index 843f1ff9..7f46fb1b 100644
--- a/src/Repository/WorkshopInstallRepository.php
+++ b/src/Repository/WorkshopInstallRepository.php
@@ -11,6 +11,9 @@ public function totalInstallsInLast30Days(Workshop $workshop): int;
 
     public function totalInstalls(Workshop $workshop): int;
 
+    /**
+     * @return array
+     */
     public function findInstallsInLast30Days(Workshop $workshop): array;
 
     public function save(WorkshopInstall $workshopInstall): void;
diff --git a/src/Service/WorkshopCreator.php b/src/Service/WorkshopCreator.php
index 112eb6c8..78027c78 100644
--- a/src/Service/WorkshopCreator.php
+++ b/src/Service/WorkshopCreator.php
@@ -28,14 +28,14 @@ public function __construct(
      *     workshop-name: string,
      *     email: string,
      *     name: string,
-     *     contact: string,
+     *     contact?: string,
      *     github-url: string
      * } $data
      * @return Workshop
      */
     public function create(array $data): Workshop
     {
-        preg_match(static::$gitHubRepoUrlRegex, $data['github-url'], $matches);
+        preg_match(self::$gitHubRepoUrlRegex, $data['github-url'], $matches);
         $owner  = $matches[3];
         $repo   = $matches[4];
 
@@ -56,17 +56,20 @@ public function create(array $data): Workshop
             $jsonData['description'],
             $data['email'],
             $data['name'],
-            $data['contact']
+            $data['contact'] ?? null
         );
 
         $this->workshopRepository->save($workshop);
         return $workshop;
     }
 
+    /**
+     * @return array
+     */
     private function getComposerJsonContents(string $owner, string $repo): array
     {
         return (array) json_decode(
-            file_get_contents(sprintf(static::$gitHubComposerJsonUrlFormat, $owner, $repo)),
+            file_get_contents(sprintf(self::$gitHubComposerJsonUrlFormat, $owner, $repo)) ?: '',
             true
         );
     }
diff --git a/src/User/AdminAuthenticationService.php b/src/User/AdminAuthenticationService.php
index 16ec028f..006025d6 100644
--- a/src/User/AdminAuthenticationService.php
+++ b/src/User/AdminAuthenticationService.php
@@ -38,12 +38,10 @@ public function hasIdentity(): bool
         return $this->authenticationService->hasIdentity();
     }
 
-    /**
-     * @psalm-suppress MixedInferredReturnType
-     * @psalm-suppress MixedReturnStatement
-     */
     public function getIdentity(): ?Admin
     {
-        return $this->authenticationService->getIdentity();
+        /** @var ?Admin $admin */
+        $admin = $this->authenticationService->getIdentity();
+        return $admin;
     }
 }
diff --git a/src/User/ArraySession.php b/src/User/ArraySession.php
index 7f0fe844..a3688add 100644
--- a/src/User/ArraySession.php
+++ b/src/User/ArraySession.php
@@ -4,16 +4,19 @@
 
 class ArraySession implements SessionStorageInterface
 {
+    /**
+     * @param array $data
+     */
     public function __construct(private array $data = [])
     {
     }
 
-    public function get(string $key, $default = null)
+    public function get(string $key, mixed $default = null): mixed
     {
         return $this->data[$key] ?? $default;
     }
 
-    public function set(string $key, $value): void
+    public function set(string $key, mixed $value): void
     {
         $this->data[$key] = $value;
     }
@@ -30,22 +33,22 @@ public function clearAll(): void
         $this->data = [];
     }
 
-    public function offsetExists($offset): bool
+    public function offsetExists(string $offset): bool
     {
         return array_key_exists($offset, $this->data);
     }
 
-    public function offsetGet(mixed $offset): mixed
+    public function offsetGet(string $offset): mixed
     {
         return $this->get($offset);
     }
 
-    public function offsetSet($offset, $value): void
+    public function offsetSet(string $offset, mixed $value): void
     {
         $this->set($offset, $value);
     }
 
-    public function offsetUnset($offset): void
+    public function offsetUnset(string $offset): void
     {
         $this->delete($offset);
     }
diff --git a/src/User/DoctrineORMStudentRepository.php b/src/User/DoctrineORMStudentRepository.php
index b305fe0e..13c9043c 100644
--- a/src/User/DoctrineORMStudentRepository.php
+++ b/src/User/DoctrineORMStudentRepository.php
@@ -6,6 +6,9 @@
 use PhpSchool\Website\User\Entity\Student;
 use Ramsey\Uuid\UuidInterface;
 
+/**
+ * @extends EntityRepository
+ */
 class DoctrineORMStudentRepository extends EntityRepository implements StudentRepository
 {
     public function findById(UuidInterface $id): ?Student
diff --git a/src/User/Entity/Admin.php b/src/User/Entity/Admin.php
index 434e952b..1ac4b252 100644
--- a/src/User/Entity/Admin.php
+++ b/src/User/Entity/Admin.php
@@ -20,7 +20,7 @@ class Admin implements \JsonSerializable
      * @ORM\GeneratedValue(strategy="CUSTOM")
      * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
      */
-    private UuidInterface $id;
+    private UuidInterface $id; /** @phpstan-ignore-line */
 
     /**
      * @ORM\Column(type="string", length=255, unique=true)
@@ -64,6 +64,13 @@ public function getPassword(): string
         return $this->passwordHash;
     }
 
+    /**
+     * @return array{
+     *     name: string,
+     *     email: string,
+     *     avatar: string
+     * }
+     */
     public function jsonSerialize(): array
     {
         return [
diff --git a/src/User/Entity/Student.php b/src/User/Entity/Student.php
index dfed19e8..762f07ec 100644
--- a/src/User/Entity/Student.php
+++ b/src/User/Entity/Student.php
@@ -10,6 +10,8 @@
 use Ramsey\Uuid\UuidInterface;
 
 /**
+ * @phpstan-import-type WorkshopState from \PhpSchool\Website\Online\StudentCloudState
+ *
  * @ORM\Entity
  * @ORM\Table(name="student"))
  * @ORM\Entity(repositoryClass="PhpSchool\Website\User\DoctrineORMStudentRepository")
@@ -24,7 +26,7 @@ class Student implements \JsonSerializable
      * @ORM\GeneratedValue(strategy="CUSTOM")
      * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
      */
-    private UuidInterface $id;
+    private UuidInterface $id; /** @phpstan-ignore-line */
 
     /**
      * @ORM\Column(type="string", length=255, unique=true)
@@ -67,10 +69,15 @@ class Student implements \JsonSerializable
     private bool $tourComplete = false;
 
     /**
+     * @var WorkshopState
+     *
      * @ORM\Column(type="json", nullable=false)
      */
     private array $workshopState = [];
 
+    /**
+     * @param WorkshopState $workshopState
+     */
     public function __construct(
         string $githubId,
         string $username,
@@ -157,11 +164,17 @@ public function setLocation(?string $location): void
         $this->location = $location;
     }
 
+    /**
+     * @return WorkshopState
+     */
     public function getWorkshopState(): array
     {
         return $this->workshopState;
     }
 
+    /**
+     * @param WorkshopState $state
+     */
     public function setWorkshopState(array $state): void
     {
         $this->workshopState = $state;
@@ -200,6 +213,21 @@ public function toDTO(): StudentDTO
         );
     }
 
+    /**
+     * @return array{
+     *     username: string,
+     *     email: string,
+     *     name: string,
+     *     profile_picture: ?string,
+     *     location: ?string,
+     *     join_date: string,
+     *     tour_complete: bool,
+     *     state: array{
+     *          workshops: WorkshopState,
+     *          total_completed: int
+     *     }
+     * }
+     */
     public function jsonSerialize(): array
     {
         return $this->toDTO()->jsonSerialize();
diff --git a/src/User/Middleware/StudentAuthenticator.php b/src/User/Middleware/StudentAuthenticator.php
index 51fa43e0..ebd38b2f 100644
--- a/src/User/Middleware/StudentAuthenticator.php
+++ b/src/User/Middleware/StudentAuthenticator.php
@@ -15,13 +15,11 @@ class StudentAuthenticator
 {
     use RedirectUtils;
 
-    private SessionStorageInterface $session;
-    private StudentRepository $studentRepository;
+    private readonly SessionStorageInterface $session;
 
-    public function __construct(SessionStorageInterface $session, StudentRepository $studentRepository)
+    public function __construct(SessionStorageInterface $session)
     {
         $this->session = $session;
-        $this->studentRepository = $studentRepository;
     }
 
     public function __invoke(Request $request, RequestHandler $handler): MessageInterface
diff --git a/src/User/Middleware/StudentRefresher.php b/src/User/Middleware/StudentRefresher.php
index bf613084..4908268d 100644
--- a/src/User/Middleware/StudentRefresher.php
+++ b/src/User/Middleware/StudentRefresher.php
@@ -27,7 +27,7 @@ public function __invoke(Request $request, RequestHandler $handler): MessageInte
     {
         $student = $this->session->get('student');
 
-        if ($student === null) {
+        if (!$student instanceof StudentDTO) {
             return $handler->handle($request);
         }
 
diff --git a/src/User/Session.php b/src/User/Session.php
index f030eeb2..eff91b72 100644
--- a/src/User/Session.php
+++ b/src/User/Session.php
@@ -2,6 +2,9 @@
 
 namespace PhpSchool\Website\User;
 
+/**
+ * @implements \ArrayAccess
+ */
 final class Session implements \ArrayAccess, SessionStorageInterface
 {
     public function regenerate(): void
@@ -18,7 +21,7 @@ public static function destroy(): void
         if (ini_get("session.use_cookies")) {
             $params = session_get_cookie_params();
             setcookie(
-                session_name(),
+                session_name() ?: 'phpschoolsess',
                 '',
                 time() - 42000,
                 (string) $params["path"],
@@ -33,12 +36,7 @@ public static function destroy(): void
         }
     }
 
-    /**
-     * @param non-empty-string $key
-     * @param mixed $default
-     * @return mixed
-     */
-    public function get(string $key, $default = null)
+    public function get(string $key, mixed $default = null): mixed
     {
         if (array_key_exists($key, $_SESSION ?? [])) {
             return $_SESSION[$key];
@@ -46,11 +44,7 @@ public function get(string $key, $default = null)
         return $default;
     }
 
-    /**
-     * @param string $key
-     * @param string|array|object|null $value
-     */
-    public function set(string $key, $value): void
+    public function set(string $key, mixed $value): void
     {
         $_SESSION[$key] = $value;
     }
@@ -67,18 +61,11 @@ public function clearAll(): void
         $_SESSION = [];
     }
 
-    /**
-     * @param string $offset
-     */
     public function offsetExists($offset): bool
     {
         return isset($_SESSION[$offset]);
     }
 
-    /**
-     * @param non-empty-string $offset
-     * @return mixed
-     */
     public function offsetGet(mixed $offset): mixed
     {
         return $this->get($offset);
@@ -86,17 +73,13 @@ public function offsetGet(mixed $offset): mixed
 
     /**
      * @param string $offset
-     * @param string|array|null $value
      */
-    public function offsetSet($offset, $value): void
+    public function offsetSet(mixed $offset, mixed $value): void
     {
         $this->set($offset, $value);
     }
 
-    /**
-     * @param string $offset
-     */
-    public function offsetUnset($offset): void
+    public function offsetUnset(mixed $offset): void
     {
         $this->delete($offset);
     }
diff --git a/src/User/SessionStorageInterface.php b/src/User/SessionStorageInterface.php
index ca8fec49..23c0494f 100644
--- a/src/User/SessionStorageInterface.php
+++ b/src/User/SessionStorageInterface.php
@@ -4,42 +4,19 @@
 
 interface SessionStorageInterface
 {
-    /**
-     * @param non-empty-string $key
-     * @param mixed $default
-     * @return mixed
-     */
-    public function get(string $key, $default = null);
-
-    /**
-     * @param string $key
-     * @param string|array|object|null $value
-     */
-    public function set(string $key, $value): void;
+    public function get(string $key, mixed $default = null): mixed;
+
+    public function set(string $key, mixed $value): void;
 
     public function delete(string $key): void;
 
     public function clearAll(): void;
 
-    /**
-     * @param string $offset
-     */
-    public function offsetExists($offset): bool;
-
-    /**
-     * @param non-empty-string $offset
-     * @return mixed
-     */
-    public function offsetGet(mixed $offset): mixed;
-
-    /**
-     * @param string $offset
-     * @param string|array|null $value
-     */
-    public function offsetSet($offset, $value): void;
-
-    /**
-     * @param string $offset
-     */
-    public function offsetUnset($offset): void;
+    public function offsetExists(string $offset): bool;
+
+    public function offsetGet(string $offset): mixed;
+
+    public function offsetSet(string $offset, mixed $value): void;
+
+    public function offsetUnset(string $offset): void;
 }
diff --git a/src/User/StudentDTO.php b/src/User/StudentDTO.php
index 20b29b3d..af1864e9 100644
--- a/src/User/StudentDTO.php
+++ b/src/User/StudentDTO.php
@@ -6,6 +6,9 @@
 use PhpSchool\Website\Online\StudentCloudState;
 use Ramsey\Uuid\UuidInterface;
 
+/**
+ * @phpstan-import-type WorkshopState from \PhpSchool\Website\Online\StudentCloudState
+ */
 class StudentDTO implements \JsonSerializable
 {
     public function __construct(
@@ -21,6 +24,21 @@ public function __construct(
     ) {
     }
 
+    /**
+     * @return array{
+     *     username: string,
+     *     email: string,
+     *     name: string,
+     *     profile_picture: ?string,
+     *     location: ?string,
+     *     join_date: string,
+     *     tour_complete: bool,
+     *     state: array{
+     *         workshops: WorkshopState,
+     *         total_completed: int
+     *     }
+     * }
+     */
     public function jsonSerialize(): array
     {
         return [
@@ -31,7 +49,7 @@ public function jsonSerialize(): array
             'location' => $this->location,
             'join_date' => $this->joinDate->format('F Y'),
             'tour_complete' => $this->tourComplete,
-            'state' => $this->workshopState
+            'state' => $this->workshopState->jsonSerialize()
         ];
     }
 }
diff --git a/src/Workshop/EmailNotifier.php b/src/Workshop/EmailNotifier.php
index 35a434e3..3e494f0a 100644
--- a/src/Workshop/EmailNotifier.php
+++ b/src/Workshop/EmailNotifier.php
@@ -10,9 +10,13 @@
 
 class EmailNotifier
 {
-    private SendGrid $sendGrid;
-    private string $phpSchoolEmail;
-    private array $templates = [
+    private SendGrid $sendGrid; /** @phpstan-ignore-line */
+    private string $phpSchoolEmail; /** @phpstan-ignore-line */
+
+    /**
+     * @var array
+     */
+    private array $templates = [ /** @phpstan-ignore-line */
         'Workshop Submitted' => 'c3c5b0c3-5059-4025-a986-7a2c763e80e8',
         'Workshop Approved' => '3f8709bd-d30d-4214-a6cd-8b92b74a6f21',
     ];
diff --git a/templates/includes/icons/apple.phtml b/templates/includes/icons/apple.phtml
deleted file mode 100644
index caec9ce7..00000000
--- a/templates/includes/icons/apple.phtml
+++ /dev/null
@@ -1 +0,0 @@
- 
\ No newline at end of file
diff --git a/templates/includes/icons/linux.phtml b/templates/includes/icons/linux.phtml
deleted file mode 100644
index e2d5567f..00000000
--- a/templates/includes/icons/linux.phtml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/templates/includes/icons/windows.phtml b/templates/includes/icons/windows.phtml
deleted file mode 100644
index 5d88bfc3..00000000
--- a/templates/includes/icons/windows.phtml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-    
-    
-    
-    
-
-
\ No newline at end of file
diff --git a/templates/includes/logo.phtml b/templates/includes/logo.phtml
deleted file mode 100644
index 73c4854f..00000000
--- a/templates/includes/logo.phtml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-    
-        
-        
-        
-        
-        
-        
-        
-    
-
\ No newline at end of file
diff --git a/templates/install.phtml b/templates/install.phtml
deleted file mode 100644
index d71fb66f..00000000
--- a/templates/install.phtml
+++ /dev/null
@@ -1,89 +0,0 @@
-fetch('includes/install-header.phtml') ?>
-
-
-
-
-
-

Operating Systems

- -
-
- fetch('includes/icons/linux.phtml') ?> -

fetch('includes/icons/linux.phtml') ?>Troubleshooting Debian Linux Installations

-

Common issues with Linux installations include not having a new enough version of PHP and not having Composer available.

-

Step 1: Check your PHP version

-

You can check your PHP version with php -v

-

If you have a PHP version less than 7.1, you will need to update it to at least 7.1, you can do so with the following commands:

-
-
-

sudo apt-get install software-properties-common

-

sudo add-apt-repository ppa:ondrej/php

-

sudo apt-get install php7.1

-
-
- -

Step 2: Check PHP School's bin directory is available in $PATH

-

After installing a workshop using the workshop manager you may find it's not available to run immediately. If this happens the simplest remedy is to make sure PHP School's workshop bin directory is available in the $PATH environment variable.

-

You can check this with workshop-manager verify which will also provide the relevant details on how to resolve the issue.

-

To learn more about the $PATH environment, click here.

- - fetch('includes/icons/apple.phtml') ?> -

- fetch('includes/icons/apple.phtml') ?> - Troubleshooting Mac OSX Installations

-

Common issues with Mac OSX installations include not having a new enough version of PHP and not having Composer available.

- -

Step 1: Check your PHP version

-

You can check your PHP version with php -v

-

If you have a PHP version less than 7.1, you will need to update it to at least 7.1, you can do so with the following commands:

-
-
-

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

-

brew tap homebrew/dupes

-

brew tap homebrew/homebrew-php

-

brew install php71

-

export PATH="$(brew --prefix php56)/bin:$PATH"

-
-
- -

Step 2: Check PHP School's bin directory is available in $PATH

-

After installing a workshop using the workshop manager you may find it's not available to run immediately. If this happens the simplest remedy is to make sure PHP School's workshop bin directory is available in the $PATH environment variable.

-

You can check this with workshop-manager verify which will also provide the relevant details on how to resolve the issue.

-

To learn more about the $PATH environment, click here.

- - fetch('includes/icons/windows.phtml') ?> -

- fetch('includes/icons/windows.phtml') ?> - Troubleshooting Windows Installations -

-

Windows is a difficult system to cater for in the PHP world. Unfortunately, it has various differences on the command line and console emulators which PHP unfortunately doesn't support. The best way to get PHP School Workshops running is to install Cygwin + ConEmu. Once the initial setup of these are complete, the process of installing workshops is the same as Linux and Mac OSX operating systems.

- -

Step 1: Check if Cygwin is installed

-

If not, follow the instructions below:

-
    -
  1. Head on over to https://cygwin.com/install.html and grab the latest installer for your system, 32-bit or 64-bit.
  2. -
  3. Run the installer and chose the default values until the package selection point.
  4. -
  5. Ensure you choose to install ALL PHP packages. We also recommend installing GIT and VIM to complete your CLI experience.
  6. -
  7. Complete the installation.
  8. -
- -

Step 2: Check if ConEmu is installed

-
    -
  1. Grab the installer from https://conemu.github.io/.
  2. -
  3. Run the installer and open ConEmu.
  4. -
  5. Select Cygwin Bash
  6. -
- -

Step 3: Check PHP School's bin directory is available in $PATH

-

After installing a workshop using the workshop manager you may find it's not available to run immediately. If this happens the simplest remedy is to make sure PHP School's workshop bin directory is available in the $PATH environment variable.

-

You can check this with workshop-manager verify which will also provide the relevant details on how to resolve the issue.

-

To learn more about the $PATH environment, click here.

- -
-
-
-
diff --git a/test/Cloud/Action/ExerciseTest.php b/test/Cloud/Action/ExerciseTest.php index 9da82bc3..84f71083 100644 --- a/test/Cloud/Action/ExerciseTest.php +++ b/test/Cloud/Action/ExerciseTest.php @@ -29,11 +29,10 @@ public function testErrorIsReturnedIfWorkshopDoesNotExist(): void ->willThrowException(new RuntimeException('Cannot find workshop')); $problemFileConverter = $this->createMock(ProblemFileConverter::class); - $renderer = $this->createMock(PhpRenderer::class); $controller = new WorkshopExercise($installedWorkshopRepo, $problemFileConverter, new ArraySession()); $request = new ServerRequest('POST', '/editor', [], json_encode([])); - $response = $controller->__invoke($request, new Response(), $renderer, 'workshop', 'exercise'); + $response = $controller->__invoke($request, new Response(), 'workshop', 'exercise'); $this->assertEquals(404, $response->getStatusCode()); $this->assertEquals([ @@ -57,11 +56,10 @@ public function testErrorIsReturnedIfExerciseDoesNotExist(): void ->willReturn($workshop); $problemFileConverter = $this->createMock(ProblemFileConverter::class); - $renderer = $this->createMock(PhpRenderer::class); $controller = new WorkshopExercise($installedWorkshopRepo, $problemFileConverter, new ArraySession()); $request = new ServerRequest('POST', '/editor', [], json_encode([])); - $response = $controller->__invoke($request, new Response(), $renderer, 'workshop', 'exercise'); + $response = $controller->__invoke($request, new Response(), 'workshop', 'exercise'); $this->assertEquals(404, $response->getStatusCode()); $this->assertEquals([ @@ -80,12 +78,10 @@ public function testWithBasicExercise(): void ->with($exercise) ->willReturn('

Some problem file

'); - $renderer = new PhpRenderer(''); - $controller = new WorkshopExercise($installedWorkshopRepo, $problemFileConverter, new ArraySession()); $request = new ServerRequest('POST', '/editor', [], json_encode([])); - $response = $controller->__invoke($request, new Response(), $renderer, 'workshop', 'exercise'); + $response = $controller->__invoke($request, new Response(), 'workshop', 'exercise'); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals([ @@ -124,17 +120,10 @@ public function testWithOfficialSolution(): void ->with($exercise) ->willReturn('

Some problem file

'); - $renderer = $this->createMock(PhpRenderer::class); - $controller = new WorkshopExercise($installedWorkshopRepo, $problemFileConverter, new ArraySession()); $request = new ServerRequest('POST', '/editor', [], json_encode([])); - $renderer->expects($this->once()) - ->method('slug') - ->with('my-exercise') - ->willReturn('my-exercise'); - - $response = $controller->__invoke($request, new Response(), $renderer, 'workshop', 'exercise'); + $response = $controller->__invoke($request, new Response(), 'workshop', 'exercise'); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals([ diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php deleted file mode 100644 index 8200185d..00000000 --- a/test/PhpRendererTest.php +++ /dev/null @@ -1,128 +0,0 @@ -render($response, 'template.phtml', ['hello' => 'Hi']); - $newResponse->getBody()->rewind(); - $this->assertEquals('Hi', $newResponse->getBody()->getContents()); - } - - public function testRenderConstructor(): void - { - $renderer = new PhpRenderer(__DIR__ . '/_files'); - $body = new Stream(fopen('php://temp', 'r+')); - $response = new Response(200, [], $body); - $newResponse = $renderer->render($response, 'template.phtml', ['hello' => 'Hi']); - $newResponse->getBody()->rewind(); - $this->assertEquals('Hi', $newResponse->getBody()->getContents()); - } - - public function testAttributeMerging(): void - { - $renderer = new PhpRenderer(__DIR__ . '/_files/', [ - 'hello' => 'Hello' - ]); - $body = new Stream(fopen('php://temp', 'r+')); - $response = new Response(200, [], $body); - $newResponse = $renderer->render($response, 'template.phtml', [ - 'hello' => 'Hi' - ]); - $newResponse->getBody()->rewind(); - $this->assertEquals('Hi', $newResponse->getBody()->getContents()); - } - - public function testExceptionInTemplate(): void - { - $renderer = new PhpRenderer(__DIR__ . '/_files/'); - $body = new Stream(fopen('php://temp', 'r+')); - $response = new Response(200, [], $body); - try { - $newResponse = $renderer->render($response, 'exception_layout.phtml'); - } catch (Throwable $t) { - // Simulates an error template - $newResponse = $renderer->render($response, 'template.phtml', [ - 'hello' => 'Hi' - ]); - } - - $newResponse->getBody()->rewind(); - $this->assertEquals('Hi', $newResponse->getBody()->getContents()); - } - - public function testExceptionForTemplateInData(): void - { - $this->expectException(InvalidArgumentException::class); - - $renderer = new PhpRenderer(__DIR__ . '/_files/'); - $body = new Stream(fopen('php://temp', 'r+')); - $response = new Response(200, [], $body); - $renderer->render($response, 'template.phtml', [ - 'template' => 'Hi' - ]); - } - - public function testTemplateNotFound(): void - { - $this->expectException(InvalidArgumentException::class); - - $renderer = new PhpRenderer(__DIR__ . '/_files/'); - $body = new Stream(fopen('php://temp', 'r+')); - $response = new Response(200, [], $body); - $renderer->render($response, 'adfadftemplate.phtml', []); - } - - public function testTemplateExists(): void - { - $renderer = new PhpRenderer(__DIR__ . '/_files/'); - $this->assertTrue($renderer->templateExists('layout.phtml')); - $this->assertFalse($renderer->templateExists('non-existant-template')); - } - - public function testAddPreload(): void - { - $renderer = new PhpRenderer(__DIR__ . '/_files/'); - $renderer->addPreload('main', '/main.js'); - $this->assertEquals(['/main.js'], $renderer->getPreload()); - } - - public function testSlug(): void - { - $renderer = new PhpRenderer(__DIR__ . '/_files/'); - - $this->assertEquals('some-string', $renderer->slug('some string')); - $this->assertEquals('Some-string', $renderer->slug('Some string')); - $this->assertEquals('Some-string', $renderer->slug('Some%string')); - } - - public function testJson(): void - { - $renderer = new PhpRenderer(__DIR__ . '/_files/'); - $this->assertEquals('[["exercise-1"],["exercise-2"]]', $renderer->json([['exercise-1'], ['exercise-2']])); - } - - public function testAddJs(): void - { - $renderer = new PhpRenderer(__DIR__ . '/_files/'); - $renderer->addJs('main', '/main.js'); - $renderer->addJs('vue.js', '/vue.js', ['async', 'type' => 'module']); - - $this->assertEquals( - [['src' => '/main.js', 'tags' => 'defer'], ['src' => '/vue.js', 'tags' => 'async type="module"']], - $renderer->getJs() - ); - } -} diff --git a/test/User/StudentDTOTest.php b/test/User/StudentDTOTest.php index 040009ee..b1666290 100644 --- a/test/User/StudentDTOTest.php +++ b/test/User/StudentDTOTest.php @@ -34,7 +34,7 @@ public function testSerialize(): void 'location' => null, 'join_date' => 'February 2022', 'tour_complete' => false, - 'state' => $state + 'state' => $state->jsonSerialize() ], $student->jsonSerialize() );