A bookmark management system demonstrating clean architecture with Middle Framework
β οΈ Development Status: Beta - undergoing QA and API stabilization before 1.0 release.
BookmarkBureau is a real-world application demonstrating how to build production-quality APIs without magic - and it's also a fully functional bookmark manager ready for personal/homelab use:
- β Type-safe throughout - Compile-time dependency injection with Middle-DI
- β Clean architecture - Action pattern, DDD principles, Repository pattern
- β Zero magic - Explicit code, no hidden behavior, full IDE support
- β High quality - SonarCloud Grade A ratings, comprehensive test coverage
- β Modern PHP - PHP 8.4 features including property hooks
This demonstrates patterns, practices, and architecture that actually work.
Dashboard View |
Search Functionality |
Admin Panel |
Want compile-time safety instead of runtime errors? Clear patterns instead of magic? Architectural discipline instead of rapid prototyping?
// Traditional PHP DI (runtime errors possible)
$service = $container->get('user.service');
// Middle-DI (compile-time safe, full IDE support)
$service = $container->getUserService();Want similar explicitness and type safety in PHP? Performance without complexity? Compiler-level guarantees?
Middle brings these philosophies to PHP.
A layer built on top of the common Controller pattern, BookmarkBureau uses a three-phase action pattern throughout:
// One line to wire up a complete CRUD operation
$router->post('/link', fn() => new ActionController(
new LinkCreateAction(
$container->getLinkService(),
new LinkInputSpec(),
new LinkOutputSpec()
),
new JsonResponseTransformer()
));Each action follows the same clear pattern:
final readonly class LinkCreateAction implements ActionInterface
{
// 1. Filter: Sanitize raw input (never throws)
public function filter(array $rawData): array
{
return $this->inputSpec->filter($rawData);
}
// 2. Validate: Check constraints (throws on invalid)
public function validate(array $data): void
{
$this->inputSpec->validate($data);
}
// 3. Execute: Perform the operation (transactional)
public function execute(array $data): array
{
$link = $this->linkService->createLink(...);
return $this->outputSpec->transform($link);
}
}Result: 30 lines of clear, testable code. No magic. Full type safety. Any developer can add new entities following this exact pattern.
- Dashboards - Container for organizing bookmarks
- Categories - Grouped collections within dashboards
- Links - Individual bookmarks with metadata
- Favorites - Quick-access links per dashboard
- Tags - Labels for organizing links
All entities have complete CRUD operations:
POST /dashboard Create dashboard
PUT /dashboard/{id} Update dashboard
DELETE /dashboard/{id} Delete dashboard
POST /category Create category
GET /category/{id} Read category
PUT /category/{id} Update category
DELETE /category/{id} Delete category
POST /link Create link
GET /link/{id} Read link
PUT /link/{id} Update link
DELETE /link/{id} Delete link
POST /dashboard/{id}/favorites Add favorite
DELETE /dashboard/{id}/favorites Remove favorite
PUT /dashboard/{id}/favorites Reorder favorites
Action Layer
- Consistent three-phase pattern (filter/validate/execute)
- InputSpec/OutputSpec for HTTP boundaries
- Zero coupling to HTTP framework
Service Layer
- Business logic coordination
- Flexible extension via OperationPipeline for cross-cutting concerns (transactions, logging, auditing)
- Clean interfaces for testing
Repository Layer
- PDO-based implementations, but interface-first and thus easily replaceable
- File-based alternatives for specific use cases (UserRepository, JwtJtiRepository)
- Entity mappers handle database in/out translation
- Cross-database compatible (MySQL/SQLite/PostgreSQL)
- Optimized queries, N+1 prevention
Configuration & DI
- Pure PHP configuration interfaces and implementations (no YAML/XML)
- Trait-based service container composition for modularity
- Compile-time type safety throughout
BookmarkBureau's domain layer is built on three complementary abstractions:
Entity β Domain objects with identity and lifecycle
Examples: User, Link, Dashboard, Category
- Mutable within transactions
- Identity is based on ID property
- Rich behaviors possible through methods
- Properties publicly accessible, but limitations possible using readonly & PHP 8.4 property hooks
Value β Immutable domain values without identity
Examples: Url, Title, HexColor, Icon, TagName
- Identity is based on full content
- Self-validating basic structure on construction (only formatting)
- Read only, cannot be changed after creation
- Fail-fast validation prevents invalid states
Composite β Type-safe compositions of entities and values
Examples: LinkCollection, CategoryWithLinks, DashboardWithCategoriesAndFavorites
- Readonly structures for returning complex data from services or repositories
- Built from domain language, not database concerns
- Aggregates for heterogeneous compositions
- Collections are a subtype for homogeneous groups
// Entity: mutable within transactions, identity-based
$link = new Link($linkId, $url, $title, ...);
$link->url = new Url('https://updated.example');
// Value: immutable, self-validating, structural equality
$url = new Url('https://example.com'); // Throws if invalid
// Composite: readonly aggregation for specific use cases
$view = new CategoryWithLinks($category, $links);This three-pillar approach creates a complete domain language: Entities represent your business concepts, Values ensure correctness, and Composites provide type-safe data structures. Together they enable compile-time safety while remaining framework-agnostic and testable.
Requirements: Docker or Podman
# Using Docker
docker run -d \
--name bookmarkbureau \
-p 8080:8080 \
-v bb-data:/var/www/var \
-e JWT_SECRET=$(openssl rand -hex 32) \
-e SITE_URL=http://localhost:8080/api.php \
ghcr.io/jschreuder/bookmark-bureau:latest
# Using Podman (rootless)
podman run -d \
--name bookmarkbureau \
-p 8080:8080 \
-v bb-data:/var/www/var \
-e JWT_SECRET=$(openssl rand -hex 32) \
-e SITE_URL=http://localhost:8080/api.php \
ghcr.io/jschreuder/bookmark-bureau:latest
# Using docker-compose
curl -O https://raw.githubusercontent.com/jschreuder/BookmarkBureau/master/docker-compose.yml
# Edit JWT_SECRET in docker-compose.yml
docker-compose up -dEnvironment Variables:
JWT_SECRET(required) - Secret key for JWT tokens (use a secure random value)SITE_URL(optional) - Base URL including/api.php, default:http://localhost:8080/api.phpSESSION_TTL(optional) - Session timeout in seconds, default:1800(30 min)TRUST_PROXY_HEADERS(optional) - Trust X-Forwarded-For headers, default:false(set totruewhen behind reverse proxy)ADMIN_IP_WHITELIST(optional) - Comma-separated IPs/CIDR ranges allowed to access admin routes, default: empty (all IPs allowed)- Example:
ADMIN_IP_WHITELIST=192.168.1.0/24,10.0.0.5restricts to local network only - Public routes (login, dashboard view) are always accessible regardless of whitelist
- Example:
Access the application at http://localhost:8080
Important: This is a demonstration project. Before exposing to the internet:
-
Behind Reverse Proxy (Required)
- Deploy behind a reverse proxy (nginx, Caddy, Synology NAS, etc.)
- Enable HTTPS at the reverse proxy level
- Set
TRUST_PROXY_HEADERS=truewhen behind a trusted reverse proxy - Ensure reverse proxy adds
X-Forwarded-Forheaders for proper rate limiting
-
JWT Secret (Critical)
- Generate a secure random secret:
openssl rand -hex 32 - Never use the default
change-me-in-productionvalue - Store securely (environment variable or secrets manager)
- Generate a secure random secret:
-
Known Limitations
- Multi-user authentication supported, but no authorization checks (all authenticated users can access/modify all data)
- Rate limiting only on login endpoints (no API endpoint throttling)
- SQLite is used (suitable for personal use, not high-traffic scenarios)
- Session/remember-me tokens valid until expiry (no server-side revocation; CLI tokens use JTI whitelist and can be revoked)
-
IP Whitelisting (Recommended for Internet Exposure)
- Restrict admin access to your local network or specific IPs
- Example:
ADMIN_IP_WHITELIST=192.168.1.0/24(allows only local network) - Supports CIDR notation and multiple ranges:
192.168.1.0/24,10.0.0.5,2001:db8::/64 - Public routes (login, dashboard view) remain accessible from anywhere
- Requires
TRUST_PROXY_HEADERS=truewhen behind reverse proxy
-
Recommended Additional Protections
- Configure firewall rules at NAS/router level
- Use fail2ban or similar for additional brute-force protection
- Monitor logs regularly (
/var/www/var/logs/) - Keep Docker images updated
Suitable for personal homelab deployments and self-hosted use. Not designed for enterprise production environments.
Requirements: PHP 8.4+, Composer, MySQL 8.0+ or SQLite
git clone https://github.com/jschreuder/BookmarkBureau.git
cd BookmarkBureau
composer install
cp config/dev.php.example config/dev.php
# Edit config/dev.php with your database credentials
vendor/bin/phinx migrateRun Development Server:
php -S localhost:8080 -t web# Create a dashboard
curl -X POST http://localhost:8080/dashboard \
-H "Content-Type: application/json" \
-d '{"title":"My Dashboard","description":"Personal bookmarks","icon":"π "}'
# Create a link
curl -X POST http://localhost:8080/link \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","title":"Example","description":"A site","icon":"π"}'
# Get a link
curl http://localhost:8080/link/{id}/src
/Action CRUD operations following three-phase pattern
/Command CLI commands for application management
/Composite Immutable data structures composing entities/values
/Config Configuration interfaces and implementations
/Controller HTTP controllers (generic ActionController)
/Entity Domain entities with value objects
/Mapper Entity-to-database mapping layer
/Exception Custom exception hierarchy
/HttpMiddleware PSR-15 HTTP middleware components
/InputSpec Request filtering and validation
/OperationMiddleware Transaction/logging middleware for service operations
/OperationPipeline Pipeline system for cross-cutting concerns
/OutputSpec Response serialization
/Repository Data access layer (interfaces + PDO/file-based)
/Response Response transformers (JSON, etc.)
/Service Business logic coordination
/ServiceContainer DI container trait-based composition
/Util Shared utilities (SqlBuilder, Filter, etc.)
*RoutingProvider.php Route registration by domain area
/migrations Database migrations
/web Application entry point
/config Configuration files
// Those using facades: magic methods, runtime resolution, hidden dependencies
$user = User::find($id);
Route::resource('products', ProductController::class);
// Middle: Explicit code, compile-time safety, dependency inversion
$user = $this->userRepository->findById($userId);
$router->post('/products', fn() => new ActionController(...));Middle wins on: Type safety, explicitness, testability Magic/conventions wins on: Speed of development, ecosystem
# Those using YAML/XML configuration
services:
App\Service\UserService:
arguments: ['@doctrine.orm.entity_manager']// Middle: Zero configuration, just PHP
public function getUserService(): UserService
{
return new UserService($this->getEntityManager());
}Middle wins on: Zero config, IDE support, simplicity Configuration-heavy wins on: Enterprise features, maturity
// Microframework: Minimal structure, bring everything yourself
$app->post('/products', function ($request, $response) {
// You build everything from scratch
});// Middle: Patterns provided, structure included
$router->post('/products', fn() => new ActionController(
new ProductCreateAction(...), // Clear pattern to follow
new JsonResponseTransformer()
));Middle wins on: Architectural patterns, DI, structure Microframework wins on: Pure minimalism, flexibility
- Middle Framework - PSR-15 routing & middleware
- Middle-DI - Compile-time dependency injection
- MiddleAuth - ACL/RBAC/ABAC authorization
This project primarily serves as a demonstration of Middle Framework patterns. However, contributions that improve the demonstration value or application quality are welcome.
MIT
Built with discipline. Designed for maintainability. Proven patterns for production.