No description
- PHP 75.1%
- CSS 18.1%
- JavaScript 5.1%
- Hack 1.7%
| assets | ||
| cache | ||
| lib | ||
| logs | ||
| templates | ||
| .gitignore | ||
| .htaccess | ||
| composer.json | ||
| composer.lock | ||
| favicon.ico | ||
| index.php | ||
| php_index.php | ||
| README.md | ||
| robots.txt | ||
ArcaneSunset
A personal poetry showcase web application, built as a lightweight PHP MVC application without any framework. Uses Composer autoloading, custom routing, PSR-7 HTTP messages, and PHP-DI for dependency injection.
Overview
| Attribute | Value |
|---|---|
| Author | ArcaneSunset contact@arcanesunset.com |
| Language | PHP (strict typing) |
| Namespace | ArcaneSunset\ → lib/ |
| Coding Style | PSR-12 (enforced via Composer scripts) |
| Entry Point | /index.php |
Dependencies
{
"composer/composer": "^2.9",
"nesbot/carbon": "^3.11",
"php-di/php-di": "^7.1",
"vlucas/phpdotenv": "^5.6",
"psr/http-message": "^2.0",
"guzzlehttp/psr7": "^2.12"
}
Directory Structure
/var/www/html/
├── index.php # Entry point (bootstraps DI, routing, rendering)
├── .htaccess # URL rewriting + AI bot blocking rules
├── .env # Environment variables (APP_DEBUG)
├── composer.json / composer.lock # Dependencies and autoload config
├── robots.txt # SEO crawling directives
├── php_index.php # Legacy/alternate entry point
├── favicon.ico # Site favicon
│
├── templates/ # Layout and partials
│ ├── main.php # Master layout (header/footer, renders page view)
│ └── components/
│ ├── headers/main.php # Header partial
│ └── footers/main.php # Footer partial
│
├── lib/.htaccess # Denies direct access to lib files
│
├── lib/ # Application code (ArcaneSunset\ namespace)
│ ├── Container.php # DI container bootstrap (php-di, file caching in prod)
│ │
│ ├── Controllers/ # Controllers
│ │ ├── AbstractController.php # Abstract base: $viewTemplate, $viewData, assign(), handle()
│ │ ├── BaseController.php # Concrete base: CRUD actions, setting context variables
│ │ ├── HomeController.php
│ │ ├── AboutController.php
│ │ └── PoemsController.php
│ │
│ ├── Models/ # Domain models and repositories
│ │ ├── Page.php # Page metadata from pages.json (title, desc, content path)
│ │ ├── BaseRepository.php # XML data fetching base class (SimpleXML → JSON decode)
│ │ ├── DummyDataProvider.php # Fallback dummy XML data generation
│ │ └── Poetry/
│ │ ├── Poem.php # Poem domain model (uses Carbon for date formatting)
│ │ └── PoetryRepository.php # Extends BaseRepository, sorted by ID desc
│ │
│ ├── Traits/ # Reusable trait compositions
│ │ └── SeasonTrait.php # Season calculation via Carbon month/day logic
│ │
│ ├── Services/ # Cross-cutting services
│ │ └── Router.php # Custom router: static route definitions with {id} placeholders
│ │
│ ├── Exceptions/ # Custom exceptions and handler
│ │ ├── ExceptionHandler.php # Centralized error handling (debug mode, logging, HTTP codes)
│ │ ├── NotFoundException.php # 404
│ │ ├── InternalServerError.php # 500
│ │ └── ValidationException.php # 400
│ │
│ ├── config/ # Configuration files
│ │ ├── config.php # App config (debug, display_errors from env)
│ │ └── di.php # PHP-DI definitions (controllers, services, factories)
│ │
│ ├── data/ # Data files
│ │ ├── pages.json # Page metadata (title, desc, content path, optional script path)
│ │ └── poetry/poems.xml # Poem data in XML format
│ │
│ └── views/ # Page-specific view templates
│ ├── index.php
│ ├── about.php
│ ├── poems.php
│ ├── poems-list.php # Alternate listing view for poems
│ └── scripts/poems.php # Page-specific inline JS
│
├── assets/ # Static assets (CSS, JS, fonts, images)
│ ├── css/main.css # Main stylesheet
│ ├── js/header.js # Header interactivity
│ ├── js/poem-modal.js # Poem modal dialog logic
│ └── fonts/ ... / img/ ... # Fonts and image assets
│
├── cache/ # DI definition cache (production only)
└── logs/ # Error log files
Architecture
Request Lifecycle
.htaccessblocks known AI bot user agents with[F]flag, then rewrites all non-file/non-directory requests to/index.php?path=$1index.phpbootstraps: loads Composer autoload, loads.envvia Dotenv, builds DI container- URL path segments are extracted directly in
index.phpusingparse_url()+ manual splitting - Segments are validated with regex (
^[a-zA-Z0-9\/_-]+$) — invalid chars throwValidationException Router::resolve()matches segments against hardcoded static route definitions, returns controller class and params- Controller is instantiated via DI container factory definitions (
$container->make()) - Route params are injected via
$controller->setRouteParams() - Controllers receive a PSR-7
ServerRequestInterfacevia constructor injection (created by GuzzleHttp in di.php) $controller->handle()executes the action and populates view dataPagemodel is instantiated with route segments to load page metadata frompages.json- If route has
jsonflag, returns JSON response; otherwise renders layout template templates/main.phpusesextract($viewData)to make controller data available as local variables in views- Page-specific view is included via
$page->getContentPath() - Page-specific scripts are included via
$page->getScriptPath()(if defined)
Controller Hierarchy
- AbstractController: Abstract base class. Defines
$viewTemplate,$viewData,$routeParams,$repository. AcceptsServerRequestInterfacein constructor. Providesassign(),getViewTemplate(),getViewData(),setRouteParams(),getRouteParam(),setRepository(). Abstracthandle()method. Uses PHP docblock@propertyand@methodannotations for IDE introspection. - BaseController: Extends AbstractController. Uses Context Traits. Adds HTTP method detection, CRUD-like actions (
getIndexAction,getViewAction,getUpdateAction), JSON path detection. Has public$id,$methodproperties and context parameters. Uses$repositoryproperty for data access. - Concrete controllers (Home, About, Poems): Extend BaseController. Set
$viewTemplateto view path.PoemsControlleroverrides constructor and sets$id = 'poem'.
Model / Repository Pattern
- Page: Loads page metadata from
lib/data/pages.json. Constructor accepts$segmentsarray (not string ID). Provides title, description, content path, script path. Validates file existence. ThrowsNotFoundException/InternalServerErroron failures. - BaseRepository: Generic XML data loader. Parses XML via SimpleXML → JSON decode → instantiates model objects. Provides
getData(),getById(),update(), sorting helpers (sortById,sortBy). Delegates dummy data writing toDummyDataProvider. - DummyDataProvider: Separate class for generating fallback XML data files. Has poem-specific (
writeDummyPoems) and generic (writeDummyDefault) methods. Both write to hardcoded path/lib/data/poetry/poems.xml. - Domain models (e.g.,
Poem): Plain PHP objects with public properties. May include formatting/parsing methods (HTML rendering, date formatting via Carbon). - Domain repositories (e.g.,
PoetryRepository): Extend BaseRepository. Configure model class and data path in constructor. Override dummy data writing for domain-specific fallback content.
Routing
Routes are hardcoded in the Router constructor:
'/' => HomeController::class,
'/about' => AboutController::class,
'/poems' => PoemsController::class,
'/poems/{id}' => PoemsController::class,
The {id} placeholder syntax is supported — segments matching placeholders are extracted as route params. The json flag in params controls JSON vs HTML output.
Dependency Injection
- Container bootstrapped in
lib/Container.phpusingphp-di/php-di - Definitions loaded from
lib/config/di.php - Controllers use factory definitions (not autowired) to avoid DI guessing repository properties
- Controllers receive
ServerRequestInterfacevia factory closure (created by GuzzleHttp\GuzzleHttp\Psr7\ServerRequest::fromGlobals()) ExceptionHandlerandRouterare autowired with constructor/property overridesPageuses a factory accepting optional$idarray parameter (route segments)PoemsControllerfactory explicitly instantiatesPoetryRepositoryand callssetRepository()- Definition caching enabled in production (
cache/directory), disabled whenAPP_DEBUG=true
Exception Handling
- Global exception handler set via
set_exception_handler()inindex.php ExceptionHandlerlogs errors tologs/error.log, sets HTTP status code, renders debug or generic error page based onAPP_DEBUG- Custom exceptions:
NotFoundException(404),InternalServerError(500),ValidationException(400)
View Rendering
- Master layout:
templates/main.php(includes header/footer partials, injects context variables into JS, loads jQuery) - Page-specific views live in
lib/views/and are included via$page->getContentPath() - Controller view data is extracted into local scope via
extract($viewData)for direct variable access in views - Views use PHP short tags (
<?php,<?=,<?=) withdeclare(strict_types=1)
Data Storage
- pages.json: JSON file defining page metadata (title, description, content path, optional script path). Supports
{id}placeholder patterns in keys (e.g.,"poems.{id}"). - poems.xml: XML file containing poem data. Parsed by BaseRepository via SimpleXML.
- Repositories write dummy/fallback data if the data file is missing (via DummyDataProvider delegation).
Coding Standards
- PSR-12 coding style enforced via
composer run cs-check/composer run cs-fix - All PHP files start with
<?phpanddeclare(strict_types=1); - Namespace follows PSR-4:
ArcaneSunset\\→lib/ - Type hints used throughout (strict typing, return types, property types)
- Extensive DocBlock annotations including
@package,@author,@since,@property, and@methodfor IDE introspection
Environment Configuration
.envfile loaded viavlucas/phpdotenvinindex.php(immutable mode)APP_DEBUGcontrols: debug error display, DI caching behavior, exception handler debug mode
Composer Scripts
| Script | Command |
|---|---|
composer run cs-check |
phpcs --standard=PSR12 . --ignore=vendor |
composer run cs-fix |
phpcbf --standard=PSR12 . --ignore=vendor |
AI Bot Protection
The .htaccess file blocks known AI/data-scraping bot user agents with an [F] (Forbidden) flag. The robots.txt file reinforces this by disallowing crawling for the same set of bots. Blocked user agents include: CCBot, ChatGPT, GPTBot, anthropic-ai, ClaudeBot, Google-CloudVertexBot, PerplexityBot, and many others.