Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
03c399e
Implement phpstan and phpunit to start pipeline CI
shenanigans-be Dec 12, 2025
35ddeb8
Give github actions a shot
shenanigans-be Dec 12, 2025
9af0ff2
Try different composer install command
shenanigans-be Dec 12, 2025
142d744
Fix paratest command
shenanigans-be Dec 12, 2025
f21406b
Fix paratest command + tiles.json bug that paratest found
shenanigans-be Dec 12, 2025
50d8f4f
Refactor slices, tiles, planets and stations
shenanigans-be Dec 13, 2025
f32207b
Try different paratest command
shenanigans-be Dec 13, 2025
4d5ebbd
Fix all tests and tiles.json
shenanigans-be Dec 13, 2025
d8b92e3
Add inline documentation about slice arrangement
shenanigans-be Dec 13, 2025
ac4446c
Refactor GeneratorConfig to DraftSettings
shenanigans-be Dec 14, 2025
515a991
Add data integrity tests to workflow
shenanigans-be Dec 14, 2025
4ab0b27
Add data integrity test for tile data and images
shenanigans-be Dec 14, 2025
a656ad4
DraftSettings tests + data integrity tests
shenanigans-be Dec 15, 2025
e96e4aa
Add decode draftsettings from JSON method
shenanigans-be Dec 16, 2025
c7840a7
Get started on draft model
shenanigans-be Dec 17, 2025
d16f972
Not using AllianceDraftSettings, it's overkill
shenanigans-be Dec 17, 2025
4c24d3e
Try to undo some damage I accidentally did while refactoring
shenanigans-be Dec 17, 2025
152ed69
Refactor Draft picks and players
shenanigans-be Dec 19, 2025
d3dcfb3
Help me I've fallen down a refactoring rabbit hole and I can't get out
shenanigans-be Dec 22, 2025
4ac0870
Start replacing the application logic with the refactored version
shenanigans-be Dec 23, 2025
72e2615
Better way to render html templates
shenanigans-be Dec 23, 2025
966ae5c
Change up slice generation and somehow fix the whole thing, hurray!
shenanigans-be Dec 24, 2025
9ee7ffa
Add handler for generate draft and tests to handle all the input
shenanigans-be Dec 24, 2025
3c2200d
I lost track of what I was doing
shenanigans-be Dec 24, 2025
26336cc
Use command handling pattern, implement some more request handlers
shenanigans-be Dec 26, 2025
052429c
CS fixes
shenanigans-be Dec 26, 2025
9c6094b
It's done. I think. I hope
shenanigans-be Dec 27, 2025
407777e
PHPStan fixes
shenanigans-be Dec 27, 2025
00bb0b1
Disable XDEbug in pipeline
shenanigans-be Dec 27, 2025
f423aa5
Make test drafts folder
shenanigans-be Dec 27, 2025
de1cfe2
Fix cs
shenanigans-be Dec 27, 2025
c7c5671
Make test drafts folder
shenanigans-be Dec 27, 2025
b763195
Refactor frontend to use tilesets and factionsets
shenanigans-be Dec 28, 2025
86ea545
cs fixes
shenanigans-be Dec 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/code-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: code-analysis

on:
pull_request:
paths-ignore:
- 'docs/**'
- '**.md'
workflow_call:

concurrency:
group: 'test-${{ github.ref_name }}'
cancel-in-progress: true

env:
APP_VERSION: ${{ format('0.0.0-build{0}', github.run_number) }}

jobs:
app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Disable Xdebug
run: sudo phpdismod xdebug
- run: composer install --prefer-dist --no-ansi --no-interaction --no-progress
- run: composer cs:check
- run: composer phpstan
32 changes: 32 additions & 0 deletions .github/workflows/test-application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: test

on:
pull_request:
paths-ignore:
- 'docs/**'
- '**.md'
workflow_call:

concurrency:
group: 'test-${{ github.ref_name }}'
cancel-in-progress: true

env:
APP_VERSION: ${{ format('0.0.0-build{0}', github.run_number) }}

jobs:
app:
runs-on: ubuntu-latest
strategy:
matrix:
testsuite:
- core
- data
steps:
- uses: actions/checkout@v5
- name: Disable Xdebug
run: sudo phpdismod xdebug
- name: Create test folder
run: mkdir tmp && mkdir tmp/test-drafts
- run: composer install --prefer-dist --no-ansi --no-interaction --no-progress
- run: composer paratest -- --processes=4 --testsuite ${{ matrix.testsuite }}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ clean.php
.DS_Store
supervisord.log
supervisord.pid
tmp
tmp
.phpunit.cache
.php-cs-fixer.cache
57 changes: 57 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

use PhpCsFixer\Config;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
use Symfony\Component\Finder\Finder;

$finder = Finder::create()
->in([
__DIR__ . '/app',
])
->name('*.php')
->ignoreDotFiles(true)
->ignoreVCS(true);

return (new Config())
->setParallelConfig(ParallelConfigFactory::detect())
->setRiskyAllowed(true)
->setRules([
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => [
'imports_order' => ['class', 'function', 'const'],
'sort_algorithm' => 'alpha',
],
'fully_qualified_strict_types' => [
'phpdoc_tags' => [],
],
'no_unused_imports' => true,
'blank_line_after_opening_tag' => true,
'declare_strict_types' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => [
'elements' => ['arguments', 'arrays', 'match', 'parameters'],
],
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'no_extra_blank_lines' => [
'tokens' => ['parenthesis_brace_block', 'return', 'square_brace_block', 'extra'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
],
'void_return' => true,
'single_quote' => true,
'multiline_promoted_properties' => [
'minimum_number_of_parameters' => 1,
],
])
->setFinder($finder);
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,26 @@ To install a local copy of this app you can clone it from the Git Repo:

Then follow these steps:

1. Add `127.0.0.1 miltydraft.test` to your `/etc/hosts` file. This first step is optional though. You can use
1. Add `127.0.0.1 milty.localhost` to your `/etc/hosts` file. This first step is optional though. You can use
127.0.0.1 directly as well.
2. Run `docker-compose up -d --build`. This will first build the image, then start all services.
3. Run `docker-compose exec app composer install`. This will install all php dependencies.
4. Create a `.env` file. See `.env.example` for details.
5. Go to [http://miltydraft.test](http://miltydraft.test) or [127.0.0.1](127.0.0.1) in your browser.
5. Go to [https://milty.localhost](https://milty.localhost) in your browser (or http://localhost if you don't want to go through the hassle of the following steps)
6. Your browser might give you some scary warnings about untrusted certificates. That's because we're using a self-signed certificate.
If you want to, you can add the certificate to your device's truster certificate.
7. Run `docker compose exec app cat /home/app/.local/share/caddy/pki/authorities/local/root.crt > caddy-cert.crt` to make a copy of the certificate in `caddy-cert.crt` (which you can then import wherever you need it)

### Libraries and Dependencies

Frontend runs on vanilla JS/jQuery (I'm aware jQuery is a bit of a blast from the past at this point; sue me and/or change it and PR me if you want) and the Back-end is vanilla PHP.
As such there's no build-system, or compiling required except for the steps described above.

To make this app as lean and mean (and easy to understand for anyone) as possible, external dependencies, both in the front- and backend should be kept to an absolute minimum.
To make this app as lean and mean (and easy to understand for anyone) as possible, external dependencies, both in the front- and backend should be kept to an absolute minimum.

### Understanding the App flow

1. Players come in on index.php and choose their options.
2. A JSON config file is created (either locally or remotely, depending on .env settings) with a unique ID
3. That Draft ID is also the Draft URL: APP_URL/d/{draft-id} (URL rewriting is done via Caddy)
3. That Draft ID is also the Draft URL: APP_URL/d/{draft-id} (URL rewriting is done via Caddy locally)
4. Players (or the Admin) make draft choices, which updates the draft json file (with very loose security, since we're assuming a very low amount of bad actors)

130 changes: 130 additions & 0 deletions app/Application.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

declare(strict_types=1);

namespace App;

use App\Draft\Repository\DraftRepository;
use App\Draft\Repository\LocalDraftRepository;
use App\Draft\Repository\S3DraftRepository;
use App\Http\ErrorResponse;
use App\Http\HttpRequest;
use App\Http\HttpResponse;
use App\Http\RequestHandler;
use App\Http\Route;
use App\Http\RouteMatch;
use App\Shared\Command;
use App\Testing\DispatcherSpy;

/**
* Unsure why I did this from scratch. I was on a bit of a refactoring roll and I couldn't resist.
*/
class Application
{
public readonly DraftRepository $repository;
private static self $instance;

public bool $spyOnDispatcher = false;
public DispatcherSpy $spy;

public function __construct()
{
if (env('STORAGE', ' local') == 'spaces') {
$this->repository = new S3DraftRepository();
} else {
$this->repository = new LocalDraftRepository();
}
}

public function run(): void
{
$response = $this->handleIncomingRequest();

http_response_code($response->code);
header('Content-type: ' . $response->getContentType());
echo $response->getBody();
exit;
}

private function handleIncomingRequest(): HttpResponse
{
try {
$handler = $this->handlerForRequest($_SERVER['REQUEST_URI']);
if ($handler == null) {
return new ErrorResponse('Page not found', 404, true);
} else {
return $handler->handle();
}

} catch (\Exception $e) {
return new ErrorResponse($e->getMessage());
}
}

private function matchToRoute(string $path): ?RouteMatch
{
$routes = include 'app/routes.php';

foreach($routes as $route => $handlerClass) {
$route = new Route($route, $handlerClass);
$match = $route->match($path);

if ($match != null) {
return $match;
}
}

return null;
}

public function handlerForRequest(string $requestUri): ?RequestHandler
{
$requestChunks = explode('?', $requestUri);

$match = $this->matchToRoute($requestChunks[0]);

if ($match == null) {
return null;
} else {
$request = HttpRequest::fromRequest($match->requestParameters);

$handler = new $match->requestHandlerClass($request);

if (! $handler instanceof RequestHandler) {
throw new \Exception('Handler does not implement RequestHandler');
}

return $handler;
}
}

public function spyOnDispatcher($commandReturnValue = null): void
{
$this->spyOnDispatcher = true;
$this->spy = new DispatcherSpy($commandReturnValue);
}

public function dontSpyOnDispatcher(): void
{
$this->spyOnDispatcher = false;
unset($this->spy);
}

public function handle(Command $command)
{
if ($this->spyOnDispatcher) {
return $this->spy->handle($command);
} else {
return $command->handle();
}
}

public static function getInstance(): self
{
if (! isset(self::$instance)) {
self::$instance = new Application();
}

return self::$instance;
}
}
82 changes: 82 additions & 0 deletions app/ApplicationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace App;

use App\Http\RequestHandlers\HandleClaimOrUnclaimPlayerRequest;
use App\Http\RequestHandlers\HandleGenerateDraftRequest;
use App\Http\RequestHandlers\HandleGetDraftRequest;
use App\Http\RequestHandlers\HandlePickRequest;
use App\Http\RequestHandlers\HandleRegenerateDraftRequest;
use App\Http\RequestHandlers\HandleRestoreClaimRequest;
use App\Http\RequestHandlers\HandleUndoRequest;
use App\Http\RequestHandlers\HandleViewDraftRequest;
use App\Http\RequestHandlers\HandleViewFormRequest;
use App\Testing\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;

/**
* @todo: fix trailing slash
*/
class ApplicationTest extends TestCase
{
public static function allRoutes()
{
yield 'For viewing the form' => [
'route' => '/',
'handler' => HandleViewFormRequest::class,
];

yield 'For viewing a draft' => [
'route' => '/d/1234',
'handler' => HandleViewDraftRequest::class,
];

yield 'For fetching draft data' => [
'route' => '/api/draft/1234',
'handler' => HandleGetDraftRequest::class,
];

yield 'For generating a draft' => [
'route' => '/api/generate',
'handler' => HandleGenerateDraftRequest::class,
];

yield 'For making a pick' => [
'route' => '/api/pick',
'handler' => HandlePickRequest::class,
];

yield 'For claiming a player' => [
'route' => '/api/claim',
'handler' => HandleClaimOrUnclaimPlayerRequest::class,
];

yield 'For restoring a claim' => [
'route' => '/api/restore',
'handler' => HandleRestoreClaimRequest::class,
];

yield 'For undoing a pick' => [
'route' => '/api/undo',
'handler' => HandleUndoRequest::class,
];

yield 'For regenerating a draft' => [
'route' => '/api/regenerate',
'handler' => HandleRegenerateDraftRequest::class,
];
}

#[Test]
#[DataProvider('allRoutes')]
public function itHasHandlerForAllRoutes($route, $handler): void
{
$application = new Application();
$determinedHandler = $application->handlerForRequest($route);

$this->assertInstanceOf($handler, $determinedHandler);
}
}
Loading