feat: Initialize Symfony application structure with essential files

- Added index.php for application entry point.
- Created app.css for global styles.
- Introduced HomeController for handling the home route.
- Implemented Kernel class for application kernel.
- Added base.html.twig as the main template.
- Created index.html.twig for the home page layout.
- Set up Docker configurations for development and production environments.
- Included opcache configuration for development.
This commit is contained in:
2026-06-13 09:06:26 -04:00
parent e69b418df9
commit d1e39aee04
36 changed files with 6893 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
FROM dunglas/frankenphp:php8.4
RUN apt-get update && apt-get install -y --no-install-recommends git unzip && rm -rf /var/lib/apt/lists/*
RUN install-php-extensions intl pdo_sqlite zip
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
# Copy dep manifests first — only reinstalls when these change
COPY app/composer.json app/composer.lock ./
RUN composer install --no-interaction --prefer-dist --no-dev --no-scripts
# Copy source and refresh autoloader with actual class paths
COPY app/ ./
RUN composer dump-autoload --optimize --no-dev
EXPOSE 80
+127
View File
@@ -0,0 +1,127 @@
.PHONY: help \
build build-docker rebuild-docker \
start start-local start-prod stop restart \
test test-watch test-coverage \
lint-check lint-fix \
install-deps reinstall-deps clean \
cache-clear cache-warm \
logs shell routes serve-info
.ONESHELL:
ROOT_DIR := $(CURDIR)
APP_DIR := $(ROOT_DIR)/app
DOCKER_CMD := docker compose -f docker-compose.yml
DOCKER_DEV_CMD := docker compose -f docker-compose.yml -f docker-compose.dev.yml
EXEC := $(DOCKER_DEV_CMD) run --rm web
BLUE := \033[0;34m
GREEN := \033[0;32m
YELLOW := \033[0;33m
NC := \033[0m
.DEFAULT_GOAL := help
help: ## Show available targets
@echo "$(BLUE)╔════════════════════════════════════════════════════════════════════╗$(NC)"
@echo "$(BLUE)║ Boathouse Cafe - Build, Test, Start & Debug Helper ║$(NC)"
@echo "$(BLUE)╚════════════════════════════════════════════════════════════════════╝$(NC)"
@echo ""
@_targets=$$(grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST)); \
_print() { echo "$$_targets" | grep -E "$$1" | awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-20s$(NC) %s\n", $$1, $$2}'; }; \
echo "$(GREEN)Build$(NC)"; _print '^build|^rebuild'; \
echo ""; \
echo "$(GREEN)Run$(NC)"; _print '^start|^stop|^restart'; \
echo ""; \
echo "$(GREEN)Test$(NC)"; _print '^test'; \
echo ""; \
echo "$(GREEN)Lint$(NC)"; _print '^lint'; \
echo ""; \
echo "$(GREEN)Dependencies$(NC)"; _print '^install|^reinstall|^clean'; \
echo ""; \
echo "$(GREEN)Database$(NC)"; _print '^db'; \
echo ""; \
echo "$(GREEN)Utilities$(NC)"; _print '^cache|^routes|^serve|^logs|^shell'
# -- Build --------------------------------------------------------------------
build: ## Install deps and clear cache
@$(EXEC) composer install
@$(EXEC) php bin/console cache:clear
build-docker: ## Build Docker image
@$(DOCKER_CMD) build
rebuild-docker: ## Rebuild Docker image from scratch and start
@$(DOCKER_CMD) down
@$(DOCKER_CMD) build --no-cache
@$(DOCKER_CMD) up -d
# -- Run ----------------------------------------------------------------------
start: ## Start Docker containers in dev mode (live reload)
@$(DOCKER_DEV_CMD) up -d
@echo "$(GREEN)Running at http://localhost:8087$(NC)"
start-local: ## Start Symfony dev server locally
@$(EXEC) symfony serve -d
@echo "$(GREEN)Running at http://localhost:8087$(NC)"
start-prod: ## Start in production mode
@$(EXEC) sh -c "APP_ENV=prod php bin/console cache:clear && symfony serve"
stop: ## Stop all services (Docker + local)
@$(DOCKER_DEV_CMD) down
restart: stop start ## Restart Docker containers
# -- Test ---------------------------------------------------------------------
test: ## Run tests
@$(EXEC) php bin/phpunit
test-watch: ## Run tests in watch mode
@$(EXEC) php bin/phpunit --testdox --watch
test-coverage: ## Generate HTML coverage report in app/coverage/
@$(EXEC) php bin/phpunit --coverage-html coverage
# -- Lint ---------------------------------------------------------------------
lint-check: ## Check code style without fixing
@$(EXEC) vendor/bin/php-cs-fixer fix --diff --dry-run
lint-fix: ## Fix code style
@$(EXEC) vendor/bin/php-cs-fixer fix
# -- Dependencies & Cleanup ---------------------------------------------------
install-deps: ## Install Composer dependencies
@$(EXEC) composer install
reinstall-deps: ## Remove vendor and reinstall dependencies
@$(EXEC) sh -c "rm -rf vendor composer.lock && composer install"
clean: ## Remove cache and logs
@$(EXEC) sh -c "rm -rf var/cache/* var/log/*"
# -- Utilities ----------------------------------------------------------------
cache-clear: ## Clear application cache
@$(EXEC) php bin/console cache:clear
cache-warm: ## Warm up application cache
@$(EXEC) php bin/console cache:warmup
routes: ## List all routes
@$(EXEC) php bin/console debug:router
serve-info: ## Show Symfony server status
@$(EXEC) symfony server:status || echo "Server not running"
logs: ## Follow Docker container logs
@$(DOCKER_DEV_CMD) logs -f
shell: ## Open shell in Docker container
@$(DOCKER_DEV_CMD) run --rm -it web sh
+64
View File
@@ -0,0 +1,64 @@
# Boathouse Cafe
A Symfony 8 web application for Boathouse Cafe.
## Stack
- **PHP 8.4** / **Symfony 8.0**
- **FrankenPHP** (web server)
- **Twig** (templating)
- **Docker** (containerized dev & prod)
## Requirements
- Docker + Docker Compose
- (Optional) PHP 8.4 + Symfony CLI for local development
## Getting Started
```bash
# Build and start in dev mode (live reload)
make start
# App runs at http://localhost:8087
```
## Common Commands
| Command | Description |
|---|---|
| `make start` | Start Docker containers in dev mode |
| `make stop` | Stop all services |
| `make restart` | Restart containers |
| `make build` | Install deps and clear cache |
| `make rebuild-docker` | Full rebuild from scratch |
| `make test` | Run test suite |
| `make lint-fix` | Fix code style |
| `make logs` | Follow container logs |
| `make shell` | Open shell in container |
Run `make help` for the full list.
## Development
```bash
# Install dependencies locally
make install-deps
# Start local Symfony dev server (no Docker)
make start-local
# Run tests
make test
# Check code style
make lint-check
```
## Production
```bash
make start-prod
```
The production Docker image uses `docker-compose.yml` only (no dev overrides). The container serves on port 80; the host maps it to `8087`.
+17
View File
@@ -0,0 +1,17 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{compose.yaml,compose.*.yaml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+27
View File
@@ -0,0 +1,27 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###
###> symfony/routing ###
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost:8087
###< symfony/routing ###
+4
View File
@@ -0,0 +1,4 @@
###> symfony/framework-bundle ###
APP_SECRET=4000805f4a6e1235f51c269550483b49
###< symfony/framework-bundle ###
+10
View File
@@ -0,0 +1,10 @@
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=changeme
###< symfony/framework-bundle ###
###> symfony/routing ###
DEFAULT_URI=http://localhost
###< symfony/routing ###
APP_SHARE_DIR=var/share
+15
View File
@@ -0,0 +1,15 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
###> friendsofphp/php-cs-fixer ###
/.php-cs-fixer.php
/.php-cs-fixer.cache
###< friendsofphp/php-cs-fixer ###
+17
View File
@@ -0,0 +1,17 @@
<?php
$finder = (new PhpCsFixer\Finder())
->in(__DIR__)
->exclude('var')
->notPath([
'config/bundles.php',
'config/reference.php',
])
;
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
])
->setFinder($finder)
;
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};
+76
View File
@@ -0,0 +1,76 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"symfony/asset": "8.0.*",
"symfony/console": "8.0.*",
"symfony/dotenv": "8.0.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "8.0.*",
"symfony/runtime": "8.0.*",
"symfony/twig-bundle": "8.0.*",
"symfony/yaml": "8.0.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"bump-after-update": true,
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*",
"symfony/polyfill-php83": "*",
"symfony/polyfill-php84": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "8.0.*"
}
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.95"
}
}
+4368
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
];
+19
View File
@@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null
+15
View File
@@ -0,0 +1,15 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
#esi: true
#fragments: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file
+10
View File
@@ -0,0 +1,10 @@
framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
default_uri: '%env(DEFAULT_URI)%'
when@prod:
framework:
router:
strict_requirements: null
+6
View File
@@ -0,0 +1,6 @@
twig:
file_name_pattern: '*.twig'
when@test:
twig:
strict_variables: true
+5
View File
@@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}
+895
View File
@@ -0,0 +1,895 @@
<?php
// This file is auto-generated and is for apps only. Bundles SHOULD NOT rely on its content.
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\Config\Loader\ParamConfigurator as Param;
/**
* This class provides array-shapes for configuring the services and bundles of an application.
*
* Services declared with the config() method below are autowired and autoconfigured by default.
*
* This is for apps only. Bundles SHOULD NOT use it.
*
* Example:
*
* ```php
* // config/services.php
* namespace Symfony\Component\DependencyInjection\Loader\Configurator;
*
* return App::config([
* 'services' => [
* 'App\\' => [
* 'resource' => '../src/',
* ],
* ],
* ]);
* ```
*
* @psalm-type ImportsConfig = list<string|array{
* resource: string,
* type?: string|null,
* ignore_errors?: bool,
* }>
* @psalm-type ParametersConfig = array<string, scalar|\UnitEnum|array<scalar|\UnitEnum|array<mixed>|Param|null>|Param|null>
* @psalm-type ArgumentsType = list<mixed>|array<string, mixed>
* @psalm-type CallType = array<string, ArgumentsType>|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool}
* @psalm-type TagsType = list<string|array<string, array<string, mixed>>> // arrays inside the list must have only one element, with the tag name as the key
* @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator
* @psalm-type DeprecationType = array{package: string, version: string, message?: string}
* @psalm-type DefaultsType = array{
* public?: bool,
* tags?: TagsType,
* resource_tags?: TagsType,
* autowire?: bool,
* autoconfigure?: bool,
* bind?: array<string, mixed>,
* }
* @psalm-type InstanceofType = array{
* shared?: bool,
* lazy?: bool|string,
* public?: bool,
* properties?: array<string, mixed>,
* configurator?: CallbackType,
* calls?: list<CallType>,
* tags?: TagsType,
* resource_tags?: TagsType,
* autowire?: bool,
* bind?: array<string, mixed>,
* constructor?: string,
* }
* @psalm-type DefinitionType = array{
* class?: string,
* file?: string,
* parent?: string,
* shared?: bool,
* synthetic?: bool,
* lazy?: bool|string,
* public?: bool,
* abstract?: bool,
* deprecated?: DeprecationType,
* factory?: CallbackType,
* configurator?: CallbackType,
* arguments?: ArgumentsType,
* properties?: array<string, mixed>,
* calls?: list<CallType>,
* tags?: TagsType,
* resource_tags?: TagsType,
* decorates?: string,
* decoration_inner_name?: string,
* decoration_priority?: int,
* decoration_on_invalid?: 'exception'|'ignore'|null,
* autowire?: bool,
* autoconfigure?: bool,
* bind?: array<string, mixed>,
* constructor?: string,
* from_callable?: CallbackType,
* }
* @psalm-type AliasType = string|array{
* alias: string,
* public?: bool,
* deprecated?: DeprecationType,
* }
* @psalm-type PrototypeType = array{
* resource: string,
* namespace?: string,
* exclude?: string|list<string>,
* parent?: string,
* shared?: bool,
* lazy?: bool|string,
* public?: bool,
* abstract?: bool,
* deprecated?: DeprecationType,
* factory?: CallbackType,
* arguments?: ArgumentsType,
* properties?: array<string, mixed>,
* configurator?: CallbackType,
* calls?: list<CallType>,
* tags?: TagsType,
* resource_tags?: TagsType,
* autowire?: bool,
* autoconfigure?: bool,
* bind?: array<string, mixed>,
* constructor?: string,
* }
* @psalm-type StackType = array{
* stack: list<DefinitionType|AliasType|PrototypeType|array<class-string, ArgumentsType|null>>,
* public?: bool,
* deprecated?: DeprecationType,
* }
* @psalm-type ServicesConfig = array{
* _defaults?: DefaultsType,
* _instanceof?: InstanceofType,
* ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null>
* }
* @psalm-type ExtensionType = array<string, mixed>
* @psalm-type FrameworkConfig = array{
* secret?: scalar|Param|null,
* http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false
* allowed_http_method_override?: null|list<string|Param>,
* trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%"
* ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%"
* test?: bool|Param,
* default_locale?: scalar|Param|null, // Default: "en"
* set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false
* set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false
* enabled_locales?: list<scalar|Param|null>,
* trusted_hosts?: string|list<scalar|Param|null>,
* trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"]
* trusted_headers?: string|list<scalar|Param|null>,
* error_controller?: scalar|Param|null, // Default: "error_controller"
* handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true
* csrf_protection?: bool|array{
* enabled?: scalar|Param|null, // Default: null
* stateless_token_ids?: list<scalar|Param|null>,
* check_header?: scalar|Param|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false
* cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token"
* },
* form?: bool|array{ // Form configuration
* enabled?: bool|Param, // Default: false
* csrf_protection?: bool|array{
* enabled?: scalar|Param|null, // Default: null
* token_id?: scalar|Param|null, // Default: null
* field_name?: scalar|Param|null, // Default: "_token"
* field_attr?: array<string, scalar|Param|null>,
* },
* },
* http_cache?: bool|array{ // HTTP cache configuration
* enabled?: bool|Param, // Default: false
* debug?: bool|Param, // Default: "%kernel.debug%"
* trace_level?: "none"|"short"|"full"|Param,
* trace_header?: scalar|Param|null,
* default_ttl?: int|Param,
* private_headers?: list<scalar|Param|null>,
* skip_response_headers?: list<scalar|Param|null>,
* allow_reload?: bool|Param,
* allow_revalidate?: bool|Param,
* stale_while_revalidate?: int|Param,
* stale_if_error?: int|Param,
* terminate_on_cache_hit?: bool|Param,
* },
* esi?: bool|array{ // ESI configuration
* enabled?: bool|Param, // Default: false
* },
* ssi?: bool|array{ // SSI configuration
* enabled?: bool|Param, // Default: false
* },
* fragments?: bool|array{ // Fragments configuration
* enabled?: bool|Param, // Default: false
* hinclude_default_template?: scalar|Param|null, // Default: null
* path?: scalar|Param|null, // Default: "/_fragment"
* },
* profiler?: bool|array{ // Profiler configuration
* enabled?: bool|Param, // Default: false
* collect?: bool|Param, // Default: true
* collect_parameter?: scalar|Param|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null
* only_exceptions?: bool|Param, // Default: false
* only_main_requests?: bool|Param, // Default: false
* dsn?: scalar|Param|null, // Default: "file:%kernel.cache_dir%/profiler"
* collect_serializer_data?: true|Param, // Default: true
* },
* workflows?: bool|array{
* enabled?: bool|Param, // Default: false
* workflows?: array<string, array{ // Default: []
* audit_trail?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* type?: "workflow"|"state_machine"|Param, // Default: "state_machine"
* marking_store?: array{
* type?: "method"|Param,
* property?: scalar|Param|null,
* service?: scalar|Param|null,
* },
* supports?: string|list<scalar|Param|null>,
* definition_validators?: list<scalar|Param|null>,
* support_strategy?: scalar|Param|null,
* initial_marking?: \BackedEnum|string|list<scalar|Param|null>,
* events_to_dispatch?: null|list<string|Param>,
* places?: string|list<array{ // Default: []
* name?: scalar|Param|null,
* metadata?: array<string, mixed>,
* }>,
* transitions?: list<array{ // Default: []
* name?: string|Param,
* guard?: string|Param, // An expression to block the transition.
* from?: \BackedEnum|string|list<array{ // Default: []
* place?: string|Param,
* weight?: int|Param, // Default: 1
* }>,
* to?: \BackedEnum|string|list<array{ // Default: []
* place?: string|Param,
* weight?: int|Param, // Default: 1
* }>,
* weight?: int|Param, // Default: 1
* metadata?: array<string, mixed>,
* }>,
* metadata?: array<string, mixed>,
* }>,
* },
* router?: bool|array{ // Router configuration
* enabled?: bool|Param, // Default: false
* resource?: scalar|Param|null,
* type?: scalar|Param|null,
* default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null
* http_port?: scalar|Param|null, // Default: 80
* https_port?: scalar|Param|null, // Default: 443
* strict_requirements?: scalar|Param|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true
* utf8?: bool|Param, // Default: true
* },
* session?: bool|array{ // Session configuration
* enabled?: bool|Param, // Default: false
* storage_factory_id?: scalar|Param|null, // Default: "session.storage.factory.native"
* handler_id?: scalar|Param|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null.
* name?: scalar|Param|null,
* cookie_lifetime?: scalar|Param|null,
* cookie_path?: scalar|Param|null,
* cookie_domain?: scalar|Param|null,
* cookie_secure?: true|false|"auto"|Param, // Default: "auto"
* cookie_httponly?: bool|Param, // Default: true
* cookie_samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax"
* use_cookies?: bool|Param,
* gc_divisor?: scalar|Param|null,
* gc_probability?: scalar|Param|null,
* gc_maxlifetime?: scalar|Param|null,
* save_path?: scalar|Param|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null.
* metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0
* },
* request?: bool|array{ // Request configuration
* enabled?: bool|Param, // Default: false
* formats?: array<string, string|list<scalar|Param|null>>,
* },
* assets?: bool|array{ // Assets configuration
* enabled?: bool|Param, // Default: true
* strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false
* version_strategy?: scalar|Param|null, // Default: null
* version?: scalar|Param|null, // Default: null
* version_format?: scalar|Param|null, // Default: "%%s?%%s"
* json_manifest_path?: scalar|Param|null, // Default: null
* base_path?: scalar|Param|null, // Default: ""
* base_urls?: string|list<scalar|Param|null>,
* packages?: array<string, array{ // Default: []
* strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false
* version_strategy?: scalar|Param|null, // Default: null
* version?: scalar|Param|null,
* version_format?: scalar|Param|null, // Default: null
* json_manifest_path?: scalar|Param|null, // Default: null
* base_path?: scalar|Param|null, // Default: ""
* base_urls?: string|list<scalar|Param|null>,
* }>,
* },
* asset_mapper?: bool|array{ // Asset Mapper configuration
* enabled?: bool|Param, // Default: false
* paths?: string|array<string, scalar|Param|null>,
* excluded_patterns?: list<scalar|Param|null>,
* exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true
* server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true
* public_prefix?: scalar|Param|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/"
* missing_import_mode?: "strict"|"warn"|"ignore"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn"
* extensions?: array<string, scalar|Param|null>,
* importmap_path?: scalar|Param|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php"
* importmap_polyfill?: scalar|Param|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims"
* importmap_script_attributes?: array<string, scalar|Param|null>,
* vendor_dir?: scalar|Param|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor"
* precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip.
* enabled?: bool|Param, // Default: false
* formats?: list<scalar|Param|null>,
* extensions?: list<scalar|Param|null>,
* },
* },
* translator?: bool|array{ // Translator configuration
* enabled?: bool|Param, // Default: false
* fallbacks?: string|list<scalar|Param|null>,
* logging?: bool|Param, // Default: false
* formatter?: scalar|Param|null, // Default: "translator.formatter.default"
* cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations"
* default_path?: scalar|Param|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations"
* paths?: list<scalar|Param|null>,
* pseudo_localization?: bool|array{
* enabled?: bool|Param, // Default: false
* accents?: bool|Param, // Default: true
* expansion_factor?: float|Param, // Default: 1.0
* brackets?: bool|Param, // Default: true
* parse_html?: bool|Param, // Default: false
* localizable_html_attributes?: list<scalar|Param|null>,
* },
* providers?: array<string, array{ // Default: []
* dsn?: scalar|Param|null,
* domains?: list<scalar|Param|null>,
* locales?: list<scalar|Param|null>,
* }>,
* globals?: array<string, string|array{ // Default: []
* value?: mixed,
* message?: string|Param,
* parameters?: array<string, scalar|Param|null>,
* domain?: string|Param,
* }>,
* },
* validation?: bool|array{ // Validation configuration
* enabled?: bool|Param, // Default: false
* enable_attributes?: bool|Param, // Default: true
* static_method?: string|list<scalar|Param|null>,
* translation_domain?: scalar|Param|null, // Default: "validators"
* email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|Param, // Default: "html5"
* mapping?: array{
* paths?: list<scalar|Param|null>,
* },
* not_compromised_password?: bool|array{
* enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true
* endpoint?: scalar|Param|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null
* },
* disable_translation?: bool|Param, // Default: false
* auto_mapping?: array<string, array{ // Default: []
* services?: list<scalar|Param|null>,
* }>,
* },
* serializer?: bool|array{ // Serializer configuration
* enabled?: bool|Param, // Default: false
* enable_attributes?: bool|Param, // Default: true
* name_converter?: scalar|Param|null,
* circular_reference_handler?: scalar|Param|null,
* max_depth_handler?: scalar|Param|null,
* mapping?: array{
* paths?: list<scalar|Param|null>,
* },
* default_context?: array<string, mixed>,
* named_serializers?: array<string, array{ // Default: []
* name_converter?: scalar|Param|null,
* default_context?: array<string, mixed>,
* include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true
* include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true
* }>,
* },
* property_access?: bool|array{ // Property access configuration
* enabled?: bool|Param, // Default: false
* magic_call?: bool|Param, // Default: false
* magic_get?: bool|Param, // Default: true
* magic_set?: bool|Param, // Default: true
* throw_exception_on_invalid_index?: bool|Param, // Default: false
* throw_exception_on_invalid_property_path?: bool|Param, // Default: true
* },
* type_info?: bool|array{ // Type info configuration
* enabled?: bool|Param, // Default: false
* aliases?: array<string, scalar|Param|null>,
* },
* property_info?: bool|array{ // Property info configuration
* enabled?: bool|Param, // Default: false
* with_constructor_extractor?: bool|Param, // Registers the constructor extractor. // Default: true
* },
* cache?: array{ // Cache configuration
* prefix_seed?: scalar|Param|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%"
* app?: scalar|Param|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem"
* system?: scalar|Param|null, // System related cache pools configuration. // Default: "cache.adapter.system"
* directory?: scalar|Param|null, // Default: "%kernel.share_dir%/pools/app"
* default_psr6_provider?: scalar|Param|null,
* default_redis_provider?: scalar|Param|null, // Default: "redis://localhost"
* default_valkey_provider?: scalar|Param|null, // Default: "valkey://localhost"
* default_memcached_provider?: scalar|Param|null, // Default: "memcached://localhost"
* default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection"
* default_pdo_provider?: scalar|Param|null, // Default: null
* pools?: array<string, array{ // Default: []
* adapters?: string|list<scalar|Param|null>,
* tags?: scalar|Param|null, // Default: null
* public?: bool|Param, // Default: false
* default_lifetime?: scalar|Param|null, // Default lifetime of the pool.
* provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter.
* early_expiration_message_bus?: scalar|Param|null,
* clearer?: scalar|Param|null,
* }>,
* },
* php_errors?: array{ // PHP errors handling configuration
* log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true
* throw?: bool|Param, // Throw PHP errors as \ErrorException instances. // Default: true
* },
* exceptions?: array<string, array{ // Default: []
* log_level?: scalar|Param|null, // The level of log message. Null to let Symfony decide. // Default: null
* status_code?: scalar|Param|null, // The status code of the response. Null or 0 to let Symfony decide. // Default: null
* log_channel?: scalar|Param|null, // The channel of log message. Null to let Symfony decide. // Default: null
* }>,
* web_link?: bool|array{ // Web links configuration
* enabled?: bool|Param, // Default: false
* },
* lock?: bool|string|array{ // Lock configuration
* enabled?: bool|Param, // Default: false
* resources?: string|array<string, string|list<scalar|Param|null>>,
* },
* semaphore?: bool|string|array{ // Semaphore configuration
* enabled?: bool|Param, // Default: false
* resources?: string|array<string, scalar|Param|null>,
* },
* messenger?: bool|array{ // Messenger configuration
* enabled?: bool|Param, // Default: false
* routing?: array<string, string|array{ // Default: []
* senders?: list<scalar|Param|null>,
* }>,
* serializer?: array{
* default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer"
* symfony_serializer?: array{
* format?: scalar|Param|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json"
* context?: array<string, mixed>,
* },
* },
* transports?: array<string, string|array{ // Default: []
* dsn?: scalar|Param|null,
* serializer?: scalar|Param|null, // Service id of a custom serializer to use. // Default: null
* options?: array<string, mixed>,
* failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
* retry_strategy?: string|array{
* service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null
* max_retries?: int|Param, // Default: 3
* delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
* multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2
* max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0
* jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1
* },
* rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null
* }>,
* failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
* stop_worker_on_signals?: int|string|list<scalar|Param|null>,
* default_bus?: scalar|Param|null, // Default: null
* buses?: array<string, array{ // Default: {"messenger.bus.default":{"default_middleware":{"enabled":true,"allow_no_handlers":false,"allow_no_senders":true},"middleware":[]}}
* default_middleware?: bool|string|array{
* enabled?: bool|Param, // Default: true
* allow_no_handlers?: bool|Param, // Default: false
* allow_no_senders?: bool|Param, // Default: true
* },
* middleware?: string|list<string|array{ // Default: []
* id?: scalar|Param|null,
* arguments?: list<mixed>,
* }>,
* }>,
* },
* scheduler?: bool|array{ // Scheduler configuration
* enabled?: bool|Param, // Default: false
* },
* disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true
* http_client?: bool|array{ // HTTP Client configuration
* enabled?: bool|Param, // Default: false
* max_host_connections?: int|Param, // The maximum number of connections to a single host.
* default_options?: array{
* headers?: array<string, mixed>,
* vars?: array<string, mixed>,
* max_redirects?: int|Param, // The maximum number of redirects to follow.
* http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version.
* resolve?: array<string, scalar|Param|null>,
* proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection.
* no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached.
* timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter.
* max_duration?: float|Param, // The maximum execution time for the request+response as a whole.
* bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to.
* verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context.
* verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name.
* cafile?: scalar|Param|null, // A certificate authority file.
* capath?: scalar|Param|null, // A directory that contains multiple certificate authority files.
* local_cert?: scalar|Param|null, // A PEM formatted certificate file.
* local_pk?: scalar|Param|null, // A private key file.
* passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file.
* ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)
* peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es).
* sha1?: mixed,
* pin-sha256?: mixed,
* md5?: mixed,
* },
* crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants.
* extra?: array<string, mixed>,
* rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null
* caching?: bool|array{ // Caching configuration.
* enabled?: bool|Param, // Default: false
* cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client"
* shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true
* max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null
* },
* retry_failed?: bool|array{
* enabled?: bool|Param, // Default: false
* retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null
* http_codes?: int|string|array<string, array{ // Default: []
* code?: int|Param,
* methods?: string|list<string|Param>,
* }>,
* max_retries?: int|Param, // Default: 3
* delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
* multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2
* max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0
* jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1
* },
* },
* mock_response_factory?: scalar|Param|null, // The id of the service that should generate mock responses. It should be either an invokable or an iterable.
* scoped_clients?: array<string, string|array{ // Default: []
* scope?: scalar|Param|null, // The regular expression that the request URL must match before adding the other options. When none is provided, the base URI is used instead.
* base_uri?: scalar|Param|null, // The URI to resolve relative URLs, following rules in RFC 3985, section 2.
* auth_basic?: scalar|Param|null, // An HTTP Basic authentication "username:password".
* auth_bearer?: scalar|Param|null, // A token enabling HTTP Bearer authorization.
* auth_ntlm?: scalar|Param|null, // A "username:password" pair to use Microsoft NTLM authentication (requires the cURL extension).
* query?: array<string, scalar|Param|null>,
* headers?: array<string, mixed>,
* max_redirects?: int|Param, // The maximum number of redirects to follow.
* http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version.
* resolve?: array<string, scalar|Param|null>,
* proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection.
* no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached.
* timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter.
* max_duration?: float|Param, // The maximum execution time for the request+response as a whole.
* bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to.
* verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context.
* verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name.
* cafile?: scalar|Param|null, // A certificate authority file.
* capath?: scalar|Param|null, // A directory that contains multiple certificate authority files.
* local_cert?: scalar|Param|null, // A PEM formatted certificate file.
* local_pk?: scalar|Param|null, // A private key file.
* passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file.
* ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...).
* peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es).
* sha1?: mixed,
* pin-sha256?: mixed,
* md5?: mixed,
* },
* crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants.
* extra?: array<string, mixed>,
* rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null
* caching?: bool|array{ // Caching configuration.
* enabled?: bool|Param, // Default: false
* cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client"
* shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true
* max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null
* },
* retry_failed?: bool|array{
* enabled?: bool|Param, // Default: false
* retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null
* http_codes?: int|string|array<string, array{ // Default: []
* code?: int|Param,
* methods?: string|list<string|Param>,
* }>,
* max_retries?: int|Param, // Default: 3
* delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
* multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2
* max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0
* jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1
* },
* }>,
* },
* mailer?: bool|array{ // Mailer configuration
* enabled?: bool|Param, // Default: false
* message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null
* dsn?: scalar|Param|null, // Default: null
* transports?: array<string, scalar|Param|null>,
* envelope?: array{ // Mailer Envelope configuration
* sender?: scalar|Param|null,
* recipients?: string|list<scalar|Param|null>,
* allowed_recipients?: string|list<scalar|Param|null>,
* },
* headers?: array<string, string|array{ // Default: []
* value?: mixed,
* }>,
* dkim_signer?: bool|array{ // DKIM signer configuration
* enabled?: bool|Param, // Default: false
* key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: ""
* domain?: scalar|Param|null, // Default: ""
* select?: scalar|Param|null, // Default: ""
* passphrase?: scalar|Param|null, // The private key passphrase // Default: ""
* options?: array<string, mixed>,
* },
* smime_signer?: bool|array{ // S/MIME signer configuration
* enabled?: bool|Param, // Default: false
* key?: scalar|Param|null, // Path to key (in PEM format) // Default: ""
* certificate?: scalar|Param|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: ""
* passphrase?: scalar|Param|null, // The private key passphrase // Default: null
* extra_certificates?: scalar|Param|null, // Default: null
* sign_options?: int|Param, // Default: null
* },
* smime_encrypter?: bool|array{ // S/MIME encrypter configuration
* enabled?: bool|Param, // Default: false
* repository?: scalar|Param|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: ""
* cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null
* },
* },
* secrets?: bool|array{
* enabled?: bool|Param, // Default: true
* vault_directory?: scalar|Param|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%"
* local_dotenv_file?: scalar|Param|null, // Default: "%kernel.project_dir%/.env.%kernel.environment%.local"
* decryption_env_var?: scalar|Param|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET"
* },
* notifier?: bool|array{ // Notifier configuration
* enabled?: bool|Param, // Default: false
* message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null
* chatter_transports?: array<string, scalar|Param|null>,
* texter_transports?: array<string, scalar|Param|null>,
* notification_on_failed_messages?: bool|Param, // Default: false
* channel_policy?: array<string, string|list<scalar|Param|null>>,
* admin_recipients?: list<array{ // Default: []
* email?: scalar|Param|null,
* phone?: scalar|Param|null, // Default: ""
* }>,
* },
* rate_limiter?: bool|array{ // Rate limiter configuration
* enabled?: bool|Param, // Default: false
* limiters?: array<string, array{ // Default: []
* lock_factory?: scalar|Param|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: "auto"
* cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
* storage_service?: scalar|Param|null, // The service ID of a custom storage implementation, this precedes any configured "cache_pool". // Default: null
* policy?: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit"|Param, // The algorithm to be used by this limiter.
* limiters?: string|list<scalar|Param|null>,
* limit?: int|Param, // The maximum allowed hits in a fixed interval or burst.
* interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
* rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket".
* interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
* amount?: int|Param, // Amount of tokens to add each interval. // Default: 1
* },
* }>,
* },
* uid?: bool|array{ // Uid configuration
* enabled?: bool|Param, // Default: false
* default_uuid_version?: 7|6|4|1|Param, // Default: 7
* name_based_uuid_version?: 5|3|Param, // Default: 5
* name_based_uuid_namespace?: scalar|Param|null,
* time_based_uuid_version?: 7|6|1|Param, // Default: 7
* time_based_uuid_node?: scalar|Param|null,
* },
* html_sanitizer?: bool|array{ // HtmlSanitizer configuration
* enabled?: bool|Param, // Default: false
* sanitizers?: array<string, array{ // Default: []
* allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
* allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
* allow_elements?: array<string, mixed>,
* block_elements?: string|list<string|Param>,
* drop_elements?: string|list<string|Param>,
* allow_attributes?: array<string, mixed>,
* drop_attributes?: array<string, mixed>,
* force_attributes?: array<string, array<string, string|Param>>,
* force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false
* allowed_link_schemes?: string|list<string|Param>,
* allowed_link_hosts?: null|string|list<string|Param>,
* allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false
* allowed_media_schemes?: string|list<string|Param>,
* allowed_media_hosts?: null|string|list<string|Param>,
* allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false
* with_attribute_sanitizers?: string|list<string|Param>,
* without_attribute_sanitizers?: string|list<string|Param>,
* max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0
* }>,
* },
* webhook?: bool|array{ // Webhook configuration
* enabled?: bool|Param, // Default: false
* message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus"
* routing?: array<string, array{ // Default: []
* service?: scalar|Param|null,
* secret?: scalar|Param|null, // Default: ""
* }>,
* },
* remote-event?: bool|array{ // RemoteEvent configuration
* enabled?: bool|Param, // Default: false
* },
* json_streamer?: bool|array{ // JSON streamer configuration
* enabled?: bool|Param, // Default: false
* },
* }
* @psalm-type TwigConfig = array{
* form_themes?: list<scalar|Param|null>,
* globals?: array<string, array{ // Default: []
* id?: scalar|Param|null,
* type?: scalar|Param|null,
* value?: mixed,
* }>,
* autoescape_service?: scalar|Param|null, // Default: null
* autoescape_service_method?: scalar|Param|null, // Default: null
* cache?: scalar|Param|null, // Default: true
* charset?: scalar|Param|null, // Default: "%kernel.charset%"
* debug?: bool|Param, // Default: "%kernel.debug%"
* strict_variables?: bool|Param, // Default: "%kernel.debug%"
* auto_reload?: scalar|Param|null,
* optimizations?: int|Param,
* default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates"
* file_name_pattern?: string|list<scalar|Param|null>,
* paths?: array<string, mixed>,
* date?: array{ // The default format options used by the date filter.
* format?: scalar|Param|null, // Default: "F j, Y H:i"
* interval_format?: scalar|Param|null, // Default: "%d days"
* timezone?: scalar|Param|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null
* },
* number_format?: array{ // The default format options for the number_format filter.
* decimals?: int|Param, // Default: 0
* decimal_point?: scalar|Param|null, // Default: "."
* thousands_separator?: scalar|Param|null, // Default: ","
* },
* mailer?: array{
* html_to_text_converter?: scalar|Param|null, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null
* },
* }
* @psalm-type TwigExtraConfig = array{
* cache?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* html?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* markdown?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* intl?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* cssinliner?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* inky?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* string?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* commonmark?: array{
* renderer?: array{ // Array of options for rendering HTML.
* block_separator?: scalar|Param|null,
* inner_separator?: scalar|Param|null,
* soft_break?: scalar|Param|null,
* },
* html_input?: "strip"|"allow"|"escape"|Param, // How to handle HTML input.
* allow_unsafe_links?: bool|Param, // Remove risky link and image URLs by setting this to false. // Default: true
* max_nesting_level?: int|Param, // The maximum nesting level for blocks. // Default: 9223372036854775807
* max_delimiters_per_line?: int|Param, // The maximum number of strong/emphasis delimiters per line. // Default: 9223372036854775807
* slug_normalizer?: array{ // Array of options for configuring how URL-safe slugs are created.
* instance?: mixed,
* max_length?: int|Param, // Default: 255
* unique?: mixed,
* },
* commonmark?: array{ // Array of options for configuring the CommonMark core extension.
* enable_em?: bool|Param, // Default: true
* enable_strong?: bool|Param, // Default: true
* use_asterisk?: bool|Param, // Default: true
* use_underscore?: bool|Param, // Default: true
* unordered_list_markers?: list<scalar|Param|null>,
* },
* ...<string, mixed>
* },
* }
* @psalm-type ConfigType = array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* framework?: FrameworkConfig,
* twig?: TwigConfig,
* twig_extra?: TwigExtraConfig,
* "when@dev"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* framework?: FrameworkConfig,
* twig?: TwigConfig,
* twig_extra?: TwigExtraConfig,
* },
* "when@prod"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* framework?: FrameworkConfig,
* twig?: TwigConfig,
* twig_extra?: TwigExtraConfig,
* },
* "when@test"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* framework?: FrameworkConfig,
* twig?: TwigConfig,
* twig_extra?: TwigExtraConfig,
* },
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* ...<string, ExtensionType>,
* }>
* }
*/
final class App
{
/**
* @param ConfigType $config
*
* @psalm-return ConfigType
*/
public static function config(array $config): array
{
/** @var ConfigType $config */
$config = AppReference::config($config);
return $config;
}
}
namespace Symfony\Component\Routing\Loader\Configurator;
/**
* This class provides array-shapes for configuring the routes of an application.
*
* Example:
*
* ```php
* // config/routes.php
* namespace Symfony\Component\Routing\Loader\Configurator;
*
* return Routes::config([
* 'controllers' => [
* 'resource' => 'routing.controllers',
* ],
* ]);
* ```
*
* @psalm-type RouteConfig = array{
* path: string|array<string,string>,
* controller?: string,
* methods?: string|list<string>,
* requirements?: array<string,string>,
* defaults?: array<string,mixed>,
* options?: array<string,mixed>,
* host?: string|array<string,string>,
* schemes?: string|list<string>,
* condition?: string,
* locale?: string,
* format?: string,
* utf8?: bool,
* stateless?: bool,
* }
* @psalm-type ImportConfig = array{
* resource: string,
* type?: string,
* exclude?: string|list<string>,
* prefix?: string|array<string,string>,
* name_prefix?: string,
* trailing_slash_on_root?: bool,
* controller?: string,
* methods?: string|list<string>,
* requirements?: array<string,string>,
* defaults?: array<string,mixed>,
* options?: array<string,mixed>,
* host?: string|array<string,string>,
* schemes?: string|list<string>,
* condition?: string,
* locale?: string,
* format?: string,
* utf8?: bool,
* stateless?: bool,
* }
* @psalm-type AliasConfig = array{
* alias: string,
* deprecated?: array{package:string, version:string, message?:string},
* }
* @psalm-type RoutesConfig = array{
* "when@dev"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
* "when@prod"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
* "when@test"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
* ...<string, RouteConfig|ImportConfig|AliasConfig>
* }
*/
final class Routes
{
/**
* @param RoutesConfig $config
*
* @psalm-return RoutesConfig
*/
public static function config(array $config): array
{
return $config;
}
}
+11
View File
@@ -0,0 +1,11 @@
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
# This file is the entry point to configure the routes of your app.
# Methods with the #[Route] attribute are automatically imported.
# See also https://symfony.com/doc/current/routing.html
# To list all registered routes, run the following command:
# bin/console debug:router
controllers:
resource: routing.controllers
+4
View File
@@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error
+23
View File
@@ -0,0 +1,23 @@
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
+21
View File
@@ -0,0 +1,21 @@
DirectoryIndex index.php
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
RewriteRule .* - [E=BASE:%1]
RewriteCond %{HTTP:Authorization} .+
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
RewriteCond %{ENV:REDIRECT_STATUS} =""
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ %{ENV:BASE}/index.php [L]
</IfModule>
Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

+9
View File
@@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return static function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
+640
View File
@@ -0,0 +1,640 @@
:root {
--cbc-navy: #0b2f55;
--cbc-deep-blue: #0f5f97;
--cbc-bright-blue: #1c9dd9;
--cbc-ice: #dff3fb;
--cbc-white: #f5fbff;
--cbc-coffee: #6a3f24;
--cbc-gold: #f2bf44;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Nunito', sans-serif;
color: var(--cbc-white);
min-height: 100vh;
background:
radial-gradient(circle at 8% 10%, #ffffff2e 0 90px, transparent 91px),
radial-gradient(circle at 88% 15%, #6ecdf533 0 140px, transparent 141px),
linear-gradient(145deg, var(--cbc-navy), var(--cbc-deep-blue) 45%, #0b426f);
}
.site-header {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.7rem 1rem;
border-bottom: 1px solid #d6f4ff40;
background: linear-gradient(180deg, #082540f0, #0b3255db);
backdrop-filter: blur(10px);
}
.brand-link {
display: inline-flex;
align-items: center;
gap: 0.55rem;
color: var(--cbc-ice);
text-decoration: none;
font-weight: 800;
letter-spacing: 0.03em;
}
.brand-icon {
width: 2rem;
height: 2rem;
border-radius: 999px;
border: 1px solid #d8f2ff85;
}
.top-nav {
display: inline-flex;
gap: 0.35rem;
flex-wrap: wrap;
}
.top-nav a {
color: var(--cbc-white);
text-decoration: none;
font-weight: 700;
border: 1px solid #dbf4ff44;
border-radius: 999px;
padding: 0.28rem 0.72rem;
background: #ffffff12;
}
.top-nav a:hover {
background: #9adcf83d;
}
.page-shell {
width: min(980px, 92vw);
margin: 3rem auto;
display: grid;
gap: 1.5rem;
}
.hero-card {
padding: 2rem;
border: 2px solid #c4ebff5c;
border-radius: 24px;
background: linear-gradient(170deg, #0a2c4dca, #0e4f80ca 65%, #1b90c2bf);
backdrop-filter: blur(8px);
box-shadow: 0 14px 44px #03102066;
text-align: center;
animation: reveal 700ms ease-out both;
}
.logo-wrap {
width: min(320px, 72vw);
margin: 0 auto 1rem;
padding: 0.55rem;
border-radius: 20px;
background: linear-gradient(135deg, #ffffff26, #70cef747);
}
.logo-image {
width: 100%;
display: block;
border-radius: 16px;
}
.tag {
margin: 0;
color: var(--cbc-gold);
letter-spacing: 0.14em;
text-transform: uppercase;
font-weight: 800;
font-size: 0.74rem;
}
h1 {
margin: 0.25rem 0 0;
font-family: 'Bebas Neue', sans-serif;
font-size: clamp(2.4rem, 8vw, 4rem);
letter-spacing: 0.05em;
color: var(--cbc-ice);
}
.lead {
margin: 0.8rem auto 1.2rem;
max-width: 55ch;
font-size: 1.08rem;
}
.pill-row {
display: flex;
justify-content: center;
gap: 0.6rem;
flex-wrap: wrap;
}
.pill {
border: 1px solid #d6f4ff7d;
border-radius: 999px;
padding: 0.35rem 0.9rem;
background: #ffffff14;
font-weight: 700;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
.info-grid article {
border-radius: 16px;
background: linear-gradient(165deg, #ffffff17, #7bc6ed26);
border: 1px solid #d8eeff47;
padding: 1rem 1.1rem;
animation: reveal 900ms ease-out both;
}
.info-grid h2 {
margin-top: 0;
color: var(--cbc-gold);
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.info-grid p {
margin: 0.45rem 0;
color: var(--cbc-white);
}
.menu-board,
.reservation-card,
.contact-card,
.about-card,
.admin-card,
.section-card {
border-radius: 16px;
background: linear-gradient(160deg, #f8fdff1a, #88d4f92b);
border: 1px solid #d8eeff47;
padding: 1rem 1.1rem;
animation: reveal 1000ms ease-out both;
}
.menu-board h2,
.reservation-card h2,
.contact-card h2,
.about-card h2,
.admin-card h2,
.section-card h2 {
margin-top: 0;
color: var(--cbc-gold);
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.menu-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.85rem;
}
.menu-grid article {
background: #0828428a;
border: 1px solid #cbeeff45;
border-radius: 12px;
padding: 0.75rem;
}
.menu-grid h3 {
margin: 0;
font-size: 1rem;
}
.menu-grid p {
margin: 0.45rem 0;
}
.menu-grid strong {
color: var(--cbc-gold);
}
.reservation-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.65rem 0.75rem;
}
.reservation-form label {
grid-column: span 2;
margin-top: 0.2rem;
font-weight: 700;
color: #dff3fb;
}
.reservation-form input {
grid-column: span 2;
border: 1px solid #bfe9ff61;
border-radius: 10px;
background: #072741c7;
color: var(--cbc-white);
padding: 0.6rem 0.72rem;
}
.reservation-form button {
grid-column: span 2;
margin-top: 0.3rem;
border: 1px solid #f5d47a66;
border-radius: 12px;
padding: 0.68rem 0.9rem;
background: linear-gradient(145deg, #f2bf44, #d29f2f);
color: #10243d;
font-weight: 800;
cursor: pointer;
}
.reservation-form button:hover {
filter: brightness(1.06);
}
.status-banner {
border-radius: 10px;
padding: 0.65rem 0.8rem;
font-weight: 700;
margin: 0.1rem 0 0.8rem;
}
.status-banner.success {
background: #2fa56c33;
border: 1px solid #8be4b866;
color: #defbe8;
}
.status-banner.error {
background: #9f2b2b40;
border: 1px solid #ffb4b499;
color: #ffe7e7;
}
.table-wrap {
overflow-x: auto;
}
.reservation-table {
width: 100%;
border-collapse: collapse;
min-width: 680px;
background: #072b4580;
border: 1px solid #bde7ff3d;
}
.reservation-table th,
.reservation-table td {
border-bottom: 1px solid #bde7ff33;
padding: 0.6rem 0.55rem;
text-align: left;
font-size: 0.94rem;
}
.reservation-table thead th {
color: var(--cbc-gold);
letter-spacing: 0.03em;
font-size: 0.86rem;
text-transform: uppercase;
}
.site-footer {
width: min(980px, 92vw);
margin: 0 auto 1.2rem;
text-align: center;
color: #d2edff;
font-size: 0.9rem;
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 860px) {
.site-header {
flex-direction: column;
align-items: flex-start;
}
.info-grid {
grid-template-columns: 1fr;
}
.menu-grid {
grid-template-columns: 1fr 1fr;
}
.reservation-form {
grid-template-columns: 1fr;
}
.reservation-form label,
.reservation-form input,
.reservation-form button {
grid-column: span 1;
}
.hero-card {
padding: 1.4rem;
}
.hours-location-grid,
.offerings-list,
.amenities-list,
.contact-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 540px) {
.menu-grid {
grid-template-columns: 1fr;
}
}
/* ── Hero CTAs ──────────────────────────────────────── */
.hero-sub {
font-size: 1.25rem;
font-weight: 700;
color: var(--cbc-ice);
margin: 0.3rem 0 0.5rem;
}
.btn-row {
display: flex;
justify-content: center;
gap: 0.75rem;
flex-wrap: wrap;
margin-top: 1.2rem;
}
.btn-primary {
display: inline-block;
padding: 0.6rem 1.4rem;
background: linear-gradient(145deg, #f2bf44, #d29f2f);
color: #10243d;
font-weight: 800;
border-radius: 999px;
text-decoration: none;
font-size: 0.95rem;
transition: filter 0.15s;
}
.btn-primary:hover {
filter: brightness(1.1);
}
.btn-secondary {
display: inline-block;
padding: 0.6rem 1.4rem;
background: transparent;
color: var(--cbc-ice);
font-weight: 700;
border-radius: 999px;
text-decoration: none;
border: 1.5px solid #c4ebff7d;
font-size: 0.95rem;
transition: background 0.15s;
}
.btn-secondary:hover {
background: #9adcf83d;
}
/* ── Section card sub-headings ──────────────────────── */
.section-card h3,
.about-card h3,
.contact-card h3,
.menu-board h3.section-sub {
color: var(--cbc-ice);
font-size: 1rem;
font-weight: 800;
margin: 1.1rem 0 0.4rem;
}
.section-card p,
.section-card li {
line-height: 1.6;
}
.contact-card a {
color: var(--cbc-bright-blue);
text-decoration: none;
}
.contact-card a:hover {
color: var(--cbc-ice);
text-decoration: underline;
}
/* ── Hours & Location ───────────────────────────────── */
.hours-location-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-top: 0.75rem;
}
.hours-table {
border-collapse: collapse;
width: 100%;
margin-top: 0.5rem;
}
.hours-table td {
padding: 0.35rem 0;
color: var(--cbc-white);
font-size: 0.95rem;
}
.hours-table td:first-child {
font-weight: 700;
color: var(--cbc-ice);
width: 58%;
}
.quiet-note {
margin-top: 0.75rem;
font-size: 0.88rem;
color: #90d0f5;
}
/* ── Offerings list ─────────────────────────────────── */
.offerings-list {
list-style: none;
padding: 0;
margin: 0.75rem 0 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.offerings-list li {
background: #0828428a;
border: 1px solid #cbeeff45;
border-radius: 12px;
padding: 0.85rem 1rem;
}
.offerings-list li strong {
display: block;
color: var(--cbc-gold);
margin-bottom: 0.35rem;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ── Menu note ──────────────────────────────────────── */
.menu-note {
margin-top: 1rem;
padding: 0.7rem 1rem;
background: #0828428a;
border: 1px solid #cbeeff45;
border-radius: 10px;
font-size: 0.9rem;
color: #b8e4f9;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
}
/* ── Workspace amenities ────────────────────────────── */
.amenities-list {
list-style: none;
padding: 0;
margin: 0.75rem 0 1rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.65rem;
}
.amenities-list li {
background: #0828428a;
border: 1px solid #cbeeff45;
border-radius: 10px;
padding: 0.75rem 0.9rem;
}
.amenities-list li strong {
display: block;
color: var(--cbc-gold);
margin-bottom: 0.2rem;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.why-list {
list-style: none;
padding: 0;
margin: 0.5rem 0 0;
}
.why-list li {
padding: 0.35rem 0 0.35rem 1.4rem;
position: relative;
color: var(--cbc-white);
line-height: 1.55;
}
.why-list li::before {
content: "→";
position: absolute;
left: 0;
color: var(--cbc-gold);
font-weight: 700;
}
/* ── Events ─────────────────────────────────────────── */
.event-item {
background: #0828428a;
border: 1px solid #cbeeff45;
border-radius: 12px;
padding: 1rem 1.1rem;
margin-top: 0.85rem;
}
.event-item h3 {
margin: 0.3rem 0 0.4rem;
color: var(--cbc-ice);
font-size: 1rem;
}
.event-meta {
font-size: 0.8rem;
color: var(--cbc-gold);
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.07em;
}
/* ── Contact grid ───────────────────────────────────── */
.contact-grid {
display: grid;
grid-template-columns: 1fr 1.3fr;
gap: 1.5rem;
margin-top: 0.75rem;
}
.form-fields-preview {
list-style: none;
padding: 0;
margin: 0.5rem 0 1rem;
}
.form-fields-preview li {
padding: 0.4rem 0.6rem;
border-bottom: 1px solid #cbeeff25;
color: #b8e4f9;
font-size: 0.9rem;
}
.social-links {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.5rem;
}
.social-link {
display: inline-block;
padding: 0.3rem 0.85rem;
border: 1px solid #d6f4ff7d;
border-radius: 999px;
background: #ffffff12;
color: var(--cbc-ice);
text-decoration: none;
font-size: 0.85rem;
font-weight: 700;
}
.social-link:hover {
background: #9adcf83d;
}
View File
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class HomeController extends AbstractController
{
#[Route('/', name: 'home')]
public function index(): Response
{
return $this->render('home/index.html.twig');
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}
+88
View File
@@ -0,0 +1,88 @@
{
"friendsofphp/php-cs-fixer": {
"version": "3.95",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.39",
"ref": "97aaf9026490db73b86c23d49e5774bc89d2b232"
},
"files": [
".php-cs-fixer.dist.php"
]
},
"symfony/console": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
},
"files": [
"bin/console"
]
},
"symfony/flex": {
"version": "2.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.4",
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
},
"files": [
".env",
".env.dev"
]
},
"symfony/framework-bundle": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.4",
"ref": "d5dcd308c8becd725c9d8b91e31aab1ff0bbc30b"
},
"files": [
"config/packages/cache.yaml",
"config/packages/framework.yaml",
"config/preload.php",
"config/routes/framework.yaml",
"config/services.yaml",
"public/index.php",
"src/Controller/.gitignore",
"src/Kernel.php",
".editorconfig"
]
},
"symfony/routing": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.4",
"ref": "bc94c4fd86f393f3ab3947c18b830ea343e51ded"
},
"files": [
"config/packages/routing.yaml",
"config/routes.yaml"
]
},
"symfony/twig-bundle": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "f250159ebe99153d0c640a3e7742876fc7453f2c"
},
"files": [
"config/packages/twig.yaml",
"templates/base.html.twig"
]
},
"twig/extra-bundle": {
"version": "v3.24.0"
}
}
+46
View File
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
<link rel="icon" href="{{ asset('images/logo.jpg') }}">
{% block stylesheets %}
{% endblock %}
{% block javascripts %}
{% endblock %}
{% set frankenphpHotReload = app.request.server.get('FRANKENPHP_HOT_RELOAD') %}
{% if frankenphpHotReload %}
<meta name="frankenphp-hot-reload:url" content="{{ frankenphpHotReload }}">
<script src="https://cdn.jsdelivr.net/npm/idiomorph"></script>
<script src="https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm" type="module"></script>
{% endif %}
</head>
<body>
<header class="site-header">
<a href="{{ path('home') }}" class="brand-link">
<img src="{{ asset('images/logo.jpg') }}" alt="Boathouse Cafe Logo" class="brand-icon">
<span>Boathouse Cafe</span>
</a>
<nav class="top-nav" aria-label="Primary">
<a href="#menu">Menu</a>
<a href="#workspace">Recharge</a>
<a href="#events">Events</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</nav>
</header>
{% block body %}{% endblock %}
<footer class="site-footer">
<p>© 2026 Boathouse Cafe Lake Hopatcong &nbsp;·&nbsp; 1 Brady Rd, Lake Hopatcong, NJ 07849 &nbsp;·&nbsp; <a href="mailto:info@boathousecafelakehopatcong.com" style="color: inherit;">info@boathousecafelakehopatcong.com</a></p>
</footer>
</body>
</html>
+256
View File
@@ -0,0 +1,256 @@
{% extends 'base.html.twig' %}
{% block title %}Boathouse Cafe — Jefferson, NJ{% endblock %}
{% block body %}
<main class="page-shell">
{# ── HERO ─────────────────────────────────────────── #}
<section class="hero-card">
<div class="logo-wrap">
<img src="{{ asset('images/logo.jpg') }}" alt="Boathouse Cafe logo" class="logo-image">
</div>
<p class="tag">Fuel for those who answer the call.</p>
<h1>Boathouse Cafe</h1>
<p class="hero-sub">A place to breathe. A place to belong.</p>
<p class="lead">
Jefferson's neighborhood cafe dedicated to first responders and healthcare professionals — because the people who show up for everyone else deserve a place that shows up for them.
</p>
<div class="btn-row">
<a href="#menu" class="btn-primary">View Menu</a>
<a href="#contact" class="btn-secondary">Host an Event</a>
</div>
</section>
{# ── HOURS & LOCATION ─────────────────────────────── #}
<section id="hours" class="section-card" aria-labelledby="hours-title">
<h2 id="hours-title">Hours &amp; Location</h2>
<div class="hours-location-grid">
<div>
<h3 class="section-sub">Open Every Week</h3>
<table class="hours-table">
<tbody>
<tr><td>Monday Friday</td><td>7:00 AM 7:00 PM</td></tr>
<tr><td>Saturday</td><td>8:00 AM 5:00 PM</td></tr>
<tr><td>Sunday</td><td>9:00 AM 4:00 PM</td></tr>
</tbody>
</table>
<p class="quiet-note">Quiet Hours: MonFri 811 AM &amp; 25 PM</p>
</div>
<div>
<h3 class="section-sub">Getting Here</h3>
<p><strong>42 Overflow Lane, Jefferson, NJ 07438</strong></p>
<p>
We're on the corner of Overflow Lane and Broadband Ave, two blocks from the Jefferson Transit Center. Street parking is available out front, and bike racks are just inside the courtyard entrance.
</p>
</div>
</div>
</section>
{# ── SIGNATURE OFFERINGS ──────────────────────────── #}
<section id="offerings" class="section-card" aria-labelledby="offerings-title">
<h2 id="offerings-title">What We Do Differently</h2>
<p>More than great coffee — a space built around the people who give the most.</p>
<ul class="offerings-list">
<li>
<strong>First Responder &amp; Healthcare Discount</strong>
15% off every order with a valid badge, ID, or hospital credentials. No questions, no hassle — just our way of saying thank you.
</li>
<li>
<strong>Pre-Shift Express Service</strong>
Running to a shift? Step up to the express counter and we'll have your order ready in under three minutes. Because your time is never yours to waste.
</li>
<li>
<strong>Decompression Lounge</strong>
A quiet, low-stimulation corner set aside for unwinding after tough calls or long shifts. Soft lighting, no screens, and a no-rush policy.
</li>
<li>
<strong>Shift-Change Happy Hour</strong>
Every day from 68 AM and 68 PM — drip coffee and tea at half price for anyone coming off or heading into a shift.
</li>
<li>
<strong>Community Board</strong>
Resources, peer support contacts, local wellness programs, and upcoming community events — all in one place, updated weekly.
</li>
<li>
<strong>Brew-Your-Way Bar</strong>
Pour-over station, cold brew on tap, and a rotating selection of single-origin beans. Have a preference? Our baristas will work with it.
</li>
</ul>
</section>
{# ── FEATURED MENU ────────────────────────────────── #}
<section id="menu" class="menu-board" aria-labelledby="menu-title">
<h2 id="menu-title">Featured Drinks</h2>
<div class="menu-grid">
<article>
<h3>Boathouse Latte</h3>
<p>Vanilla cold foam, double espresso, oat milk. Rich, smooth, and ready the moment you need it most.</p>
<strong>$5.75</strong>
</article>
<article>
<h3>First Response Roast</h3>
<p>Single-origin pour-over with a bright citrus finish. Clean, clear, and built for the kind of focus that saves lives.</p>
<strong>$4.50</strong>
</article>
<article>
<h3>Night Shift Mocha</h3>
<p>Dark chocolate, double shot, whipped cream. For the long hours, the quiet halls, and the ones who stay when everyone else goes home.</p>
<strong>$6.25</strong>
</article>
<article>
<h3>Trauma Bay Flat White</h3>
<p>Ristretto shots, steamed whole milk, served in a 5 oz cup. Compact, concentrated, and always steady under pressure.</p>
<strong>$5.50</strong>
</article>
<article>
<h3>Off-Duty Cold Brew</h3>
<p>18-hour steep, served over ice. Slow-built, smooth, and exactly what a hard shift deserves on the other side.</p>
<strong>$5.00</strong>
</article>
<article>
<h3>Dispatch Americano</h3>
<p>Two shots, hot water, no fuss. Clean and direct — because your radio doesn't wait and neither should your coffee.</p>
<strong>$4.25</strong>
</article>
<article>
<h3>Station House Macchiato</h3>
<p>Espresso, butterscotch drizzle, steamed oat milk. A small comfort that hits harder than it looks.</p>
<strong>$5.75</strong>
</article>
<article>
<h3>Triage Chai</h3>
<p>Spiced masala chai with steamed whole milk. Grounding, warm, and reliable — just like the best people on your team.</p>
<strong>$4.75</strong>
</article>
</div>
<div class="menu-note">
<span>Full menu available in-store.</span>
<a href="#contact" class="btn-secondary" style="font-size: 0.85rem; padding: 0.3rem 0.9rem;">Download Full Menu</a>
</div>
</section>
{# ── RECHARGE ─────────────────────────────────────── #}
<section id="workspace" class="section-card" aria-labelledby="workspace-title">
<h2 id="workspace-title">A Space to Recharge</h2>
<p>Whether you're coming off a 12-hour shift, squeezing in a break between calls, or just need somewhere quiet to land — we've got you.</p>
<ul class="amenities-list">
<li>
<div>
<strong>No Rush Policy</strong>
Stay as long as you need. We will never hurry you out the door. Your time off is yours.
</div>
</li>
<li>
<div>
<strong>Decompression Corner</strong>
A low-stimulation zone with soft lighting, comfortable seating, and a standing "no loud conversations" rule. A real place to come down.
</div>
</li>
<li>
<div>
<strong>Comfortable Seating for Every Need</strong>
Lounge chairs, padded booths, and counter seats — because after hours on your feet, the chair matters.
</div>
</li>
<li>
<div>
<strong>Quiet Hours</strong>
MonFri, 811 AM and 25 PM. Low music, no loudspeakers, and a calm environment by design.
</div>
</li>
<li>
<div>
<strong>Background Playlist</strong>
Ambient and instrumental mixes kept at a volume that fades into the background rather than demanding your attention.
</div>
</li>
</ul>
<h3 class="section-sub">Why First Responders &amp; Healthcare Workers Choose Us</h3>
<ul class="why-list">
<li>15% discount — no app, no loyalty card, just your ID.</li>
<li>Express service for pre-shift and between-call stops.</li>
<li>A staff that understands the weight of what you carry.</li>
</ul>
</section>
{# ── EVENTS & COMMUNITY ───────────────────────────── #}
<section id="events" class="section-card" aria-labelledby="events-title">
<h2 id="events-title">Events &amp; Community</h2>
<p>A gathering place for the people who keep Jefferson safe, healthy, and whole.</p>
<div class="event-item">
<div class="event-meta">Weekly &nbsp;·&nbsp; Wednesdays, 6:008:00 PM</div>
<h3>Peer Support Circle</h3>
<p>An informal, open-door gathering for first responders and healthcare workers to connect, decompress, and share — no agenda, no pressure. Facilitated by community volunteers and supported by local peer support organizations. Coffee and light snacks on us.</p>
</div>
<div class="event-item">
<div class="event-meta">Monthly &nbsp;·&nbsp; Second Saturday, 10:00 AM12:00 PM</div>
<h3>Wellness Morning</h3>
<p>A monthly Saturday dedicated to wellbeing. We partner with local instructors and counselors to offer short wellness sessions — breathwork, stress management, and mental health resources — followed by open conversation and a complimentary cup of your choice.</p>
</div>
<div class="event-item">
<div class="event-meta">Ongoing &nbsp;·&nbsp; Sunday Mornings</div>
<h3>Continuing Education Corner</h3>
<p>We reserve a quiet section on Sunday mornings for study and CE coursework. Group discounts on drip coffee, a guaranteed calm environment, and a staff that will leave you alone unless you need a refill. Reach out to reserve your spot.</p>
</div>
<div class="event-item">
<h3>Reserve for Your Station or Unit</h3>
<p>Boathouse Cafe is available for private gatherings — station dinners, departmental meetups, team debriefs, or just a casual crew night out. Our main floor seats up to 40 guests with a private ordering tab for your group. First responder and healthcare group rates apply. Reach out at least one week in advance for larger bookings.</p>
<a href="#contact" class="btn-primary" style="display: inline-block; margin-top: 0.75rem; font-size: 0.9rem;">Book a Reservation</a>
</div>
</section>
{# ── ABOUT ────────────────────────────────────────── #}
<section id="about" class="about-card" aria-labelledby="about-title">
<h2 id="about-title">Our Story</h2>
<p>
Boathouse Cafe was built around a simple idea: the people who run toward emergencies deserve a place that takes care of them in return. We're a family with deep roots in emergency services and healthcare, and we know firsthand what a long shift feels like — and what it means to finally have somewhere to sit down.
</p>
<p>
We opened our doors in Jefferson, NJ with a short menu, a warm space, and a commitment to the nurses, paramedics, firefighters, police officers, dispatchers, doctors, and every other essential worker who keeps this community safe. The name is a nod to those critical moments — and to the people who stay calm and capable when a code blue is called.
</p>
<p>
Today, Boathouse Cafe is more than a coffee shop. It's where off-duty officers come to breathe. Where night-shift nurses stop in before dawn. Where firefighters gather after a hard call. We don't ask about your shift. We just make sure your cup is full, your seat is comfortable, and you feel like you're somewhere that's genuinely glad you're here.
</p>
</section>
{# ── CONTACT ──────────────────────────────────────── #}
<section id="contact" class="contact-card" aria-labelledby="contact-title">
<h2 id="contact-title">Get in Touch</h2>
<p>Whether you have a question, want to book an event, or just want to say hello — we'd love to hear from you.</p>
<div class="contact-grid">
<div>
<h3>Contact Info</h3>
<p><a href="mailto:info@boathousecafelakehopatcong.com">info@boathousecafelakehopatcong.com</a></p>
<p><a href="https://boathousecafelakehopatcong.com" target="_blank" rel="noreferrer">boathousecafelakehopatcong.com</a></p>
<h3>Follow Us</h3>
<p>Find us on your favorite platforms — search "Boathouse Cafe."</p>
<div class="social-links">
<span class="social-link">GitHub</span>
<span class="social-link">Instagram</span>
<span class="social-link">LinkedIn</span>
</div>
</div>
<div>
<h3>Send a Message</h3>
<p style="font-size: 0.88rem; color: #90d0f5; margin: 0.2rem 0 0.6rem;">For event bookings, include your preferred date and estimated group size.</p>
<ul class="form-fields-preview">
<li>Name</li>
<li>Email</li>
<li>Message</li>
<li>Preferred event date <em>(if booking)</em></li>
</ul>
<a href="mailto:info@boathousecafelakehopatcong.com" class="btn-primary">Send Message</a>
<p style="font-size: 0.82rem; color: #6bbfe0; margin-top: 0.85rem;">Online ordering coming soon.</p>
</div>
</div>
</section>
</main>
{% endblock %}
+16
View File
@@ -0,0 +1,16 @@
services:
web:
environment:
APP_ENV: dev
APP_DEBUG: "1"
SERVER_NAME: ":80"
volumes:
- ./app:/app
- dev_cache:/app/var/cache
- dev_log:/app/var/log
- ./docker/opcache-dev.ini:/usr/local/etc/php/conf.d/opcache-dev.ini:ro
command: sh -c "composer install --no-interaction && frankenphp run --config /etc/frankenphp/Caddyfile --adapter caddyfile"
volumes:
dev_cache:
dev_log:
+6
View File
@@ -0,0 +1,6 @@
services:
web:
environment:
APP_ENV: prod
APP_DEBUG: "0"
RESERVATION_DB_PATH: /data/reservations.sqlite
+20
View File
@@ -0,0 +1,20 @@
services:
web:
build:
context: .
dockerfile: Dockerfile
container_name: boathouse-cafe-web
ports:
- "8087:80"
environment:
APP_ENV: prod
APP_DEBUG: "0"
SERVER_NAME: ":80"
working_dir: /app
networks:
default:
driver: bridge
ipam:
config:
- subnet: 10.10.100.0/24
+2
View File
@@ -0,0 +1,2 @@
opcache.validate_timestamps=1
opcache.revalidate_freq=0