init
Some checks are pending
CI / Tests (push) Waiting to run
CI / Docker Lint (push) Waiting to run

This commit is contained in:
sluppyy 2025-02-13 13:39:42 +03:00
parent a8ba145c3f
commit e1937c9e82
58 changed files with 8754 additions and 5 deletions

View File

@ -1,7 +1,7 @@
#syntax=docker/dockerfile:1 #syntax=docker/dockerfile:1
# Versions # Versions
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream
# The different stages of this Dockerfile are meant to be built into separate images # The different stages of this Dockerfile are meant to be built into separate images
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage # https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
@ -39,6 +39,9 @@ ENV COMPOSER_ALLOW_SUPERUSER=1
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d" ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
###> recipes ### ###> recipes ###
###> doctrine/doctrine-bundle ###
RUN install-php-extensions pdo_pgsql
###< doctrine/doctrine-bundle ###
###< recipes ### ###< recipes ###
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/ COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
@ -60,6 +63,7 @@ RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
RUN set -eux; \ RUN set -eux; \
install-php-extensions \ install-php-extensions \
xdebug \ xdebug \
pgsql \
; ;
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/ COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1
assets/app.js Normal file
View File

@ -0,0 +1 @@
import './styles/app.css';

19
assets/styles/app.css Normal file
View File

@ -0,0 +1,19 @@
:root {
--primary-color: #f6cd26;
}
body {
background-color: black;
color: var(--primary-color);
}
.block {
border: solid var(--primary-color) 2px;
padding: 12px;
}
.list {
display: flex;
flex-direction: column;
gap: 12px;
}

21
bin/console Executable file
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);
};

View File

@ -22,3 +22,9 @@ services:
###> symfony/mercure-bundle ### ###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ###
database:
ports:
- "5432"
###< doctrine/doctrine-bundle ###

View File

@ -7,14 +7,11 @@ services:
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM # Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8} DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-app}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-16}&charset=${POSTGRES_CHARSET:-utf8}
# Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration # Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure} MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}:${HTTPS_PORT:-443}/.well-known/mercure} MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}:${HTTPS_PORT:-443}/.well-known/mercure}
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
# The two next lines can be removed after initial installation
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
STABILITY: ${STABILITY:-stable}
volumes: volumes:
- caddy_data:/data - caddy_data:/data
- caddy_config:/config - caddy_config:/config
@ -36,8 +33,31 @@ services:
###> symfony/mercure-bundle ### ###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ###
database:
image: postgres:${POSTGRES_VERSION:-16}-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
# You should definitely change the password in production
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app}
healthcheck:
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
timeout: 5s
retries: 5
start_period: 60s
volumes:
- database_data:/var/lib/postgresql/data:rw
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
# - ./docker/db/data:/var/lib/postgresql/data:rw
###< doctrine/doctrine-bundle ###
volumes: volumes:
caddy_data: caddy_data:
caddy_config: caddy_config:
###> symfony/mercure-bundle ### ###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###

91
composer.json Normal file
View File

@ -0,0 +1,91 @@
{
"name": "symfony/skeleton",
"type": "project",
"license": "MIT",
"description": "A minimal Symfony project recommended to create bare bones applications",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.4.3",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.3",
"easycorp/easyadmin-bundle": "^4.24",
"runtime/frankenphp-symfony": "^0.2.0",
"symfony/asset": "7.2.*",
"symfony/asset-mapper": "7.2.*",
"symfony/console": "7.2.*",
"symfony/dotenv": "7.2.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "7.2.*",
"symfony/mercure-bundle": "^0.3.9",
"symfony/runtime": "7.2.*",
"symfony/translation": "7.2.*",
"symfony/twig-bundle": "7.2.*",
"symfony/ux-twig-component": "^2.23",
"symfony/yaml": "7.2.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/intl-extra": "^3.19",
"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": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.2.*",
"docker": true
}
},
"require-dev": {
"symfony/maker-bundle": "^1.62",
"symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*"
}
}

7316
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
config/bundles.php Normal file
View File

@ -0,0 +1,15 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
];

View File

@ -0,0 +1,11 @@
framework:
asset_mapper:
# The paths to make available to the asset mapper.
paths:
- assets/
missing_import_mode: strict
when@prod:
framework:
asset_mapper:
missing_import_mode: warn

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

11
config/packages/csrf.yaml Normal file
View File

@ -0,0 +1,11 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout

View File

@ -0,0 +1,54 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
controller_resolver:
auto_mapping: false
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

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

View File

@ -0,0 +1,8 @@
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'

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: http://localhost
when@prod:
framework:
router:
strict_requirements: null

View File

@ -0,0 +1,39 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

View File

@ -0,0 +1,7 @@
framework:
default_locale: ru
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- ru
providers:

View File

@ -0,0 +1,6 @@
twig:
file_name_pattern: '*.twig'
when@test:
twig:
strict_variables: true

View File

@ -0,0 +1,5 @@
twig_component:
anonymous_template_directory: 'components/'
defaults:
# Namespace & directory for components
App\Twig\Components\: 'components/'

View File

@ -0,0 +1,11 @@
framework:
validation:
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false

View File

@ -0,0 +1,17 @@
when@dev:
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler:
only_exceptions: false
collect_serializer_data: true
when@test:
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler: { collect: false }

5
config/preload.php Normal file
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';
}

5
config/routes.yaml Normal file
View File

@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute

View File

@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View File

@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service

View File

@ -0,0 +1,8 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler

24
config/services.yaml Normal file
View File

@ -0,0 +1,24 @@
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# 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/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

19
importmap.php Normal file
View File

@ -0,0 +1,19 @@
<?php
/**
* Returns the importmap for this application.
*
* - "path" is a path inside the asset mapper system. Use the
* "debug:asset-map" command to see the full list of paths.
*
* - "entrypoint" (JavaScript only) set to true for any module that will
* be used as an "entrypoint" (and passed to the importmap() Twig function).
*
* The "importmap:require" command can be used to add new entries to this file.
*/
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
];

0
migrations/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250212203002 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE branch (id SERIAL NOT NULL, theme_id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_BB861B1F59027487 ON branch (theme_id)');
$this->addSql('CREATE TABLE message (id SERIAL NOT NULL, branch_id INT NOT NULL, text TEXT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_B6BD307FDCD6CC49 ON message (branch_id)');
$this->addSql('CREATE TABLE theme (id SERIAL NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('ALTER TABLE branch ADD CONSTRAINT FK_BB861B1F59027487 FOREIGN KEY (theme_id) REFERENCES theme (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE message ADD CONSTRAINT FK_B6BD307FDCD6CC49 FOREIGN KEY (branch_id) REFERENCES branch (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE branch DROP CONSTRAINT FK_BB861B1F59027487');
$this->addSql('ALTER TABLE message DROP CONSTRAINT FK_B6BD307FDCD6CC49');
$this->addSql('DROP TABLE branch');
$this->addSql('DROP TABLE message');
$this->addSql('DROP TABLE theme');
}
}

9
public/index.php Normal file
View File

@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

0
src/Controller/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,24 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Branch;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class BranchCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Branch::class;
}
public function configureFields(string $pageName): iterable
{
yield AssociationField::new('theme');
yield TextField::new('name');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Controller\Admin;
use App\Controller\ThemeController;
use App\Entity\Branch;
use App\Entity\Message;
use App\Entity\Theme;
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
//#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
class DashboardController extends AbstractDashboardController
{
#[Route('/admin', name: 'admin')]
public function index(): Response
{
$routeBuilder = $this->container->get(AdminUrlGenerator::class);
$url = $routeBuilder->setController(ThemeCrudController::class)->generateUrl();
return $this->redirect($url);
}
public function configureDashboard(): Dashboard
{
return Dashboard::new()
->setTitle('Forum');
}
public function configureMenuItems(): iterable
{
// yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');
yield MenuItem::linktoRoute('Website', 'fas fa-home', 'themes');
yield MenuItem::linkToCrud('Themes', 'fas fa-book', Theme::class);
yield MenuItem::linkToCrud('Branches', 'fas fa-cube', Branch::class);
yield MenuItem::linkToCrud('Messages', 'fas fa-commenting', Message::class);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Message;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
class MessageCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Message::class;
}
public function configureFields(string $pageName): iterable
{
yield AssociationField::new('branch');
yield TextareaField::new('text');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Theme;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class ThemeCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Theme::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Controller;
use App\Entity\Branch;
use App\Entity\Message;
use App\Form\CreateMessageType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/branches')]
final class BranchController extends AbstractController
{
public function __construct(private readonly EntityManagerInterface $entityManager)
{
}
#[Route('/{id}', name: 'branch', methods: ['GET'])]
public function index(Branch $branch): Response
{
return $this->render('branch/show.html.twig', [
'branch' => $branch,
'create_message_form' => $this->createForm(CreateMessageType::class),
]);
}
#[Route('/{id}', name: 'branch.create', methods: ['POST'])]
public function create(Branch $branch, Request $request): Response
{
$message = new Message();
$form = $this->createForm(CreateMessageType::class, $message);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$message->setBranch($branch);
$this->entityManager->persist($message);
$this->entityManager->flush();
}
return $this->redirectToRoute('branch', ['id' => $branch->getId()]);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Controller;
use App\Entity\Theme;
use App\Repository\ThemeRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ThemeController extends AbstractController
{
#[Route('/', name: 'themes')]
public function index(ThemeRepository $repository): Response
{
return $this->render('theme/index.html.twig', [
'themes' => $repository->findAll(),
]);
}
#[Route('/themes/{id}', name: 'theme')]
public function show(Theme $theme): Response
{
return $this->render('theme/show.html.twig', [
'theme' => $theme,
]);
}
}

0
src/Entity/.gitignore vendored Normal file
View File

101
src/Entity/Branch.php Normal file
View File

@ -0,0 +1,101 @@
<?php
namespace App\Entity;
use App\Repository\BranchRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: BranchRepository::class)]
class Branch
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
private ?string $name = null;
#[ORM\ManyToOne(inversedBy: 'branches')]
#[ORM\JoinColumn(nullable: false)]
private ?Theme $theme = null;
/**
* @var Collection<int, Message>
*/
#[ORM\OneToMany(targetEntity: Message::class, mappedBy: 'branch', orphanRemoval: true)]
private Collection $messages;
public function __construct()
{
$this->messages = new ArrayCollection();
}
public function __toString(): string
{
return $this->name;
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getTheme(): ?Theme
{
return $this->theme;
}
public function setTheme(?Theme $theme): static
{
$this->theme = $theme;
return $this;
}
/**
* @return Collection<int, Message>
*/
public function getMessages(): Collection
{
return $this->messages;
}
public function addMessage(Message $message): static
{
if (!$this->messages->contains($message)) {
$this->messages->add($message);
$message->setBranch($this);
}
return $this;
}
public function removeMessage(Message $message): static
{
if ($this->messages->removeElement($message)) {
// set the owning side to null (unless already changed)
if ($message->getBranch() === $this) {
$message->setBranch(null);
}
}
return $this;
}
}

52
src/Entity/Message.php Normal file
View File

@ -0,0 +1,52 @@
<?php
namespace App\Entity;
use App\Repository\MessageRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MessageRepository::class)]
class Message
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $text = null;
#[ORM\ManyToOne(inversedBy: 'messages')]
#[ORM\JoinColumn(nullable: false)]
private ?Branch $branch = null;
public function getId(): ?int
{
return $this->id;
}
public function getText(): ?string
{
return $this->text;
}
public function setText(string $text): static
{
$this->text = $text;
return $this;
}
public function getBranch(): ?Branch
{
return $this->branch;
}
public function setBranch(?Branch $branch): static
{
$this->branch = $branch;
return $this;
}
}

83
src/Entity/Theme.php Normal file
View File

@ -0,0 +1,83 @@
<?php
namespace App\Entity;
use App\Repository\ThemeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ThemeRepository::class)]
class Theme
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
/**
* @var Collection<int, Branch>
*/
#[ORM\OneToMany(targetEntity: Branch::class, mappedBy: 'theme', orphanRemoval: true)]
private Collection $branches;
public function __construct()
{
$this->branches = new ArrayCollection();
}
public function __toString(): string
{
return $this->name;
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/**
* @return Collection<int, Branch>
*/
public function getBranches(): Collection
{
return $this->branches;
}
public function addBranch(Branch $branch): static
{
if (!$this->branches->contains($branch)) {
$this->branches->add($branch);
$branch->setTheme($this);
}
return $this;
}
public function removeBranch(Branch $branch): static
{
if ($this->branches->removeElement($branch)) {
// set the owning side to null (unless already changed)
if ($branch->getTheme() === $this) {
$branch->setTheme(null);
}
}
return $this;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Form;
use App\Entity\Branch;
use App\Entity\Message;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CreateMessageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('text')
->add('submit', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Message::class,
]);
}
}

11
src/Kernel.php Normal file
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;
}

0
src/Repository/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Branch;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Branch>
*/
class BranchRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Branch::class);
}
// /**
// * @return Branch[] Returns an array of Branch objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('b.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Branch
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Message;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Message>
*/
class MessageRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Message::class);
}
// /**
// * @return Message[] Returns an array of Message objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('m.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Message
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Theme;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Theme>
*/
class ThemeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Theme::class);
}
// /**
// * @return Theme[] Returns an array of Theme objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('t')
// ->andWhere('t.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('t.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Theme
// {
// return $this->createQueryBuilder('t')
// ->andWhere('t.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

231
symfony.lock Normal file
View File

@ -0,0 +1,231 @@
{
"doctrine/doctrine-bundle": {
"version": "2.13",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.13",
"ref": "8d96c0b51591ffc26794d865ba3ee7d193438a83"
},
"files": [
"config/packages/doctrine.yaml",
"src/Entity/.gitignore",
"src/Repository/.gitignore"
]
},
"doctrine/doctrine-migrations-bundle": {
"version": "3.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.1",
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
},
"files": [
"config/packages/doctrine_migrations.yaml",
"migrations/.gitignore"
]
},
"easycorp/easyadmin-bundle": {
"version": "4.24",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.0",
"ref": "b131e6cbfe1b898a508987851963fff485986285"
}
},
"symfony/asset-mapper": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/asset_mapper.yaml",
"importmap.php"
]
},
"symfony/console": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
},
"files": [
"bin/console"
]
},
"symfony/flex": {
"version": "2.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.4",
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
},
"files": [
".env",
".env.dev"
]
},
"symfony/form": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
},
"files": [
"config/packages/csrf.yaml"
]
},
"symfony/framework-bundle": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "87bcf6f7c55201f345d8895deda46d2adbdbaa89"
},
"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"
]
},
"symfony/maker-bundle": {
"version": "1.62",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/mercure-bundle": {
"version": "0.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "0.3",
"ref": "528285147494380298f8f991ee8c47abebaf79db"
},
"files": [
"config/packages/mercure.yaml"
]
},
"symfony/routing": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "21b72649d5622d8f7da329ffb5afb232a023619d"
},
"files": [
"config/packages/routing.yaml",
"config/routes.yaml"
]
},
"symfony/security-bundle": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
},
"files": [
"config/packages/security.yaml",
"config/routes/security.yaml"
]
},
"symfony/translation": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b"
},
"files": [
"config/packages/translation.yaml",
"translations/.gitignore"
]
},
"symfony/twig-bundle": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
},
"files": [
"config/packages/twig.yaml",
"templates/base.html.twig"
]
},
"symfony/uid": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
}
},
"symfony/ux-twig-component": {
"version": "2.23",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.13",
"ref": "67814b5f9794798b885cec9d3f48631424449a01"
},
"files": [
"config/packages/twig_component.yaml"
]
},
"symfony/validator": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
},
"files": [
"config/packages/validator.yaml"
]
},
"symfony/web-profiler-bundle": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.1",
"ref": "e42b3f0177df239add25373083a564e5ead4e13a"
},
"files": [
"config/packages/web_profiler.yaml",
"config/routes/web_profiler.yaml"
]
},
"twig/extra-bundle": {
"version": "v3.19.0"
}
}

17
templates/base.html.twig Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %}
{% endblock %}
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,18 @@
{% extends 'base.html.twig' %}
{% block title %}{{ branch.name }}{% endblock %}
{% block body %}
<h1>{{ branch.name }}</h1>
<div class="list">
{% for message in branch.messages %}
<span class="block">
{{ message.text }}
</span>
{% endfor %}
</div>
<h2>{% trans %}words.message{% endtrans %}</h2>
{{ form(create_message_form) }}
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html.twig' %}
{% block title %}{% trans %}titles.themes{% endtrans %}{% endblock %}
{% block body %}
<h1>{% trans %}titles.themes{% endtrans %}</h1>
<ul>
{% for theme in themes %}
<li>
<a href="{{ path('theme', { id: theme.id }) }}">
{{ theme.name }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends 'base.html.twig' %}
{% block title %}{% trans %}titles.theme{% endtrans %} {{ theme.name }}{% endblock %}
{% block body %}
<h1>{{ theme.name }}</h1>
<ul>
{% for branch in theme.branches %}
<li><a href="{{ path('branch', { id: branch.id }) }}">{{ branch.name }}</a></li>
{% endfor %}
</ul>
{% endblock %}

0
translations/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,7 @@
titles:
themes: Темы
words:
message: Сообщение
Text: Текст
Submit: Подтвердить