feat(controller): add post lists controller

This commit is contained in:
Ondrej Vlach 2024-08-03 10:47:43 +02:00
parent 58f43f096a
commit a99f6244b4
No known key found for this signature in database
GPG Key ID: 7F141CDACEDEE2DE
32 changed files with 8956 additions and 447 deletions

19
.gitignore vendored
View File

@ -1,4 +1,3 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
@ -18,8 +17,18 @@
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###
###> squizlabs/php_codesniffer ###
/.phpcs-cache
/phpcs.xml
###< squizlabs/php_codesniffer ###
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###

View File

@ -1,4 +1,3 @@
import './bootstrap.js';
/*
* Welcome to your app's main JavaScript file!
*
@ -6,5 +5,7 @@ import './bootstrap.js';
* which should already be in your base.html.twig.
*/
import './styles/app.css';
import './styles/global.scss';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
require('bootstrap');

5
assets/bootstrap.js vendored
View File

@ -1,5 +0,0 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

View File

@ -1,15 +0,0 @@
{
"controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": false,
"fetch": "eager"
}
}
},
"entrypoints": []
}

View File

@ -1,16 +0,0 @@
import { Controller } from '@hotwired/stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}

View File

@ -1,3 +0,0 @@
body {
background-color: skyblue;
}

View File

@ -0,0 +1,3 @@
$primary: darken(#66531e, 20%);
@import "~bootstrap/scss/bootstrap";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
assets/vendor/installed.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?php return array (
'@hotwired/stimulus' =>
array (
'version' => '3.2.2',
'dependencies' =>
array (
),
'extraFiles' =>
array (
),
),
'@hotwired/turbo' =>
array (
'version' => '7.3.0',
'dependencies' =>
array (
),
'extraFiles' =>
array (
),
),
);

View File

@ -16,7 +16,6 @@
"phpdocumentor/reflection-docblock": "^5.4",
"phpstan/phpdoc-parser": "^1.29",
"symfony/asset": "7.1.*",
"symfony/asset-mapper": "7.1.*",
"symfony/console": "7.1.*",
"symfony/doctrine-messenger": "7.1.*",
"symfony/dotenv": "7.1.*",
@ -37,15 +36,15 @@
"symfony/runtime": "7.1.*",
"symfony/security-bundle": "7.1.*",
"symfony/serializer": "7.1.*",
"symfony/stimulus-bundle": "^2.18",
"symfony/string": "7.1.*",
"symfony/translation": "7.1.*",
"symfony/twig-bundle": "7.1.*",
"symfony/ux-turbo": "^2.18",
"symfony/validator": "7.1.*",
"symfony/web-link": "7.1.*",
"symfony/webpack-encore-bundle": "^2.1",
"symfony/yaml": "7.1.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/string-extra": "^3.10",
"twig/twig": "^2.12|^3.0"
},
"config": {
@ -80,8 +79,7 @@
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"

466
composer.lock generated
View File

@ -4,89 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "131c365156daf6fd97b5e15f509f17eb",
"content-hash": "3e172a49e6d5bdaae8e4224364e75043",
"packages": [
{
"name": "composer/semver",
"version": "3.4.2",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6",
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.4",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-07-12T11:35:52+00:00"
},
{
"name": "doctrine/cache",
"version": "2.2.0",
@ -2178,85 +2097,6 @@
],
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/asset-mapper",
"version": "v7.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/asset-mapper.git",
"reference": "b496ba0c1ca69abbbc2413b853decad2eed9a74b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/asset-mapper/zipball/b496ba0c1ca69abbbc2413b853decad2eed9a74b",
"reference": "b496ba0c1ca69abbbc2413b853decad2eed9a74b",
"shasum": ""
},
"require": {
"composer/semver": "^3.0",
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/filesystem": "^7.1",
"symfony/http-client": "^6.4|^7.0"
},
"conflict": {
"symfony/framework-bundle": "<6.4"
},
"require-dev": {
"symfony/asset": "^6.4|^7.0",
"symfony/browser-kit": "^6.4|^7.0",
"symfony/console": "^6.4|^7.0",
"symfony/event-dispatcher-contracts": "^3.0",
"symfony/finder": "^6.4|^7.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/web-link": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\AssetMapper\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Maps directories of assets & makes them available in a public directory with versioned filenames.",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/asset-mapper/tree/v7.1.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-07-09T19:36:07+00:00"
},
{
"name": "symfony/cache",
"version": "v7.1.3",
@ -6352,75 +6192,6 @@
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/stimulus-bundle",
"version": "v2.18.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/stimulus-bundle.git",
"reference": "017b60e036c366c8ce0e77864d5aabab436ad73d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/017b60e036c366c8ce0e77864d5aabab436ad73d",
"reference": "017b60e036c366c8ce0e77864d5aabab436ad73d",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/deprecation-contracts": "^2.0|^3.0",
"symfony/finder": "^5.4|^6.0|^7.0",
"symfony/http-kernel": "^5.4|^6.0|^7.0",
"twig/twig": "^2.15.3|^3.8"
},
"require-dev": {
"symfony/asset-mapper": "^6.3|^7.0",
"symfony/framework-bundle": "^5.4|^6.0|^7.0",
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
"symfony/twig-bundle": "^5.4|^6.0|^7.0",
"zenstruck/browser": "^1.4"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Symfony\\UX\\StimulusBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Integration with your Symfony app & Stimulus!",
"keywords": [
"symfony-ux"
],
"support": {
"source": "https://github.com/symfony/stimulus-bundle/tree/v2.18.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-11T13:21:54+00:00"
},
{
"name": "symfony/stopwatch",
"version": "v7.1.1",
@ -7017,103 +6788,6 @@
],
"time": "2024-05-31T14:59:31+00:00"
},
{
"name": "symfony/ux-turbo",
"version": "v2.18.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-turbo.git",
"reference": "e447231ddcc09ab68d29047f47d31a524837dc7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-turbo/zipball/e447231ddcc09ab68d29047f47d31a524837dc7a",
"reference": "e447231ddcc09ab68d29047f47d31a524837dc7a",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/stimulus-bundle": "^2.9.1"
},
"conflict": {
"symfony/flex": "<1.13"
},
"require-dev": {
"dbrekelmans/bdi": "dev-main",
"doctrine/doctrine-bundle": "^2.4.3",
"doctrine/orm": "^2.8 | 3.0",
"phpstan/phpstan": "^1.10",
"symfony/debug-bundle": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/form": "^5.4|^6.0|^7.0",
"symfony/framework-bundle": "^5.4|^6.0|^7.0",
"symfony/mercure-bundle": "^0.3.7",
"symfony/messenger": "^5.4|^6.0|^7.0",
"symfony/panther": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|6.3.*|^7.0",
"symfony/property-access": "^5.4|^6.0|^7.0",
"symfony/security-core": "^5.4|^6.0|^7.0",
"symfony/stopwatch": "^5.4|^6.0|^7.0",
"symfony/twig-bundle": "^5.4|^6.0|^7.0",
"symfony/web-profiler-bundle": "^5.4|^6.0|^7.0",
"symfony/webpack-encore-bundle": "^2.1.1"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"name": "symfony/ux",
"url": "https://github.com/symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\Turbo\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "kevin@dunglas.fr"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Hotwire Turbo integration for Symfony",
"homepage": "https://symfony.com",
"keywords": [
"hotwire",
"javascript",
"mercure",
"symfony-ux",
"turbo",
"turbo-stream"
],
"support": {
"source": "https://github.com/symfony/ux-turbo/tree/v2.18.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-01T17:56:14+00:00"
},
{
"name": "symfony/validator",
"version": "v7.1.3",
@ -7453,6 +7127,77 @@
],
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/webpack-encore-bundle",
"version": "v2.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/webpack-encore-bundle.git",
"reference": "75cb918df3f65e28cf0d4bc03042bc45ccb19dd0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/75cb918df3f65e28cf0d4bc03042bc45ccb19dd0",
"reference": "75cb918df3f65e28cf0d4bc03042bc45ccb19dd0",
"shasum": ""
},
"require": {
"php": ">=8.1.0",
"symfony/asset": "^5.4 || ^6.2 || ^7.0",
"symfony/config": "^5.4 || ^6.2 || ^7.0",
"symfony/dependency-injection": "^5.4 || ^6.2 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.2 || ^7.0",
"symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0"
},
"require-dev": {
"symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0",
"symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0",
"symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0",
"symfony/web-link": "^5.4 || ^6.2 || ^7.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"name": "symfony/webpack-encore",
"url": "https://github.com/symfony/webpack-encore"
}
},
"autoload": {
"psr-4": {
"Symfony\\WebpackEncoreBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Integration with your Symfony app & Webpack Encore!",
"support": {
"issues": "https://github.com/symfony/webpack-encore-bundle/issues",
"source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.1.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-10-22T18:53:08+00:00"
},
{
"name": "symfony/yaml",
"version": "v7.1.1",
@ -7598,6 +7343,73 @@
],
"time": "2024-05-11T07:35:57+00:00"
},
{
"name": "twig/string-extra",
"version": "v3.10.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/string-extra.git",
"reference": "cd76ed8ae081bcd4fddf549e92e20c5df76c358a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/string-extra/zipball/cd76ed8ae081bcd4fddf549e92e20c5df76c358a",
"reference": "cd76ed8ae081bcd4fddf549e92e20c5df76c358a",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/string": "^5.4|^6.4|^7.0",
"symfony/translation-contracts": "^1.1|^2|^3",
"twig/twig": "^3.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Twig\\Extra\\String\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
}
],
"description": "A Twig extension for Symfony String",
"homepage": "https://twig.symfony.com",
"keywords": [
"html",
"string",
"twig",
"unicode"
],
"support": {
"source": "https://github.com/twigphp/string-extra/tree/v3.10.0"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2024-05-11T07:35:57+00:00"
},
{
"name": "twig/twig",
"version": "v3.10.3",

View File

@ -7,10 +7,9 @@ return [
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
];

View File

@ -1,5 +0,0 @@
framework:
asset_mapper:
# The paths to make available to the asset mapper.
paths:
- assets/

View File

@ -0,0 +1,45 @@
webpack_encore:
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
# Uncomment (also under link_attributes) if using Turbo Drive
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
# 'data-turbo-track': reload
# link_attributes:
# Uncomment if using Turbo Drive
# 'data-turbo-track': reload
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# If you have multiple builds:
# builds:
# frontend: '%kernel.project_dir%/public/frontend/build'
# pass the build name as the 3rd argument to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
#when@prod:
# webpack_encore:
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# # Available in version 1.2
# cache: true
#when@test:
# webpack_encore:
# strict_mode: false

View File

@ -4,6 +4,8 @@
# 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:
list.page.size: 24
list.page.max_visible_pages: 3
services:
# default configuration for services in *this* file
@ -23,5 +25,11 @@ services:
App\Command\RefreshDatabaseCommand:
arguments:
$name: "brilo:refresh-database"
App\Controller\ListPostController:
arguments:
$pageSize: '%list.page.size%'
$maxVisiblePages: '%list.page.max_visible_pages%'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

View File

@ -1,3 +1,7 @@
parameters:
list.page.size: 3
list.page.max_visible_pages: 3
services:
App\Service\Remote\BriloApiUsers:
public: true

View File

@ -1,28 +0,0 @@
<?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,
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'@hotwired/turbo' => [
'version' => '7.3.0',
],
];

8252
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"devDependencies": {
"@popperjs/core": "^2.11.8",
"@symfony/webpack-encore": "^4.6.1",
"bootstrap": "^5.3.3",
"jquery": "^3.7.1",
"sass": "^1.77.8",
"sass-loader": "^14.2.1",
"webpack-notifier": "^1.15.0"
},
"dependencies": {
"webpack": "^5.93.0"
}
}

View File

@ -21,6 +21,12 @@
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="db">
<directory>tests/Db</directory>
</testsuite>
<testsuite name="web">
<directory>tests/Web</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>

View File

@ -0,0 +1,8 @@
<?php
namespace App\Controller;
class DetailPostController
{
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\Post\PostRepository;
use App\Utils\Paginator\Paginator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
final class ListPostController extends AbstractController
{
public function __construct(
private readonly int $pageSize,
private readonly int $maxVisiblePages,
private readonly PostRepository $postRepository,
private readonly Paginator $paginator,
) {
}
#[Route('/', name: 'default')]
public function default(): Response
{
return $this->forward(ListPostController::class . '::pageList', ['page' => 1]);
}
#[Route('/posts/{page}', name: 'page_list', requirements: ['page' => '\d+'], defaults: ['page' => 1])]
public function pageList(int $page): Response
{
$totalPages = (int) ceil($this->postRepository->count([]) / $this->pageSize); /* TODO: tohle by si zaslouzilo asi cache */
if ($totalPages === 0) {
$this->logger->error('No posts found. Try run brilo:refresh-database command.');
throw new NotFoundHttpException('No posts found.');
}
if ($page === 0) {
return $this->redirectToRoute('page_list', ['page' => 1]);
}
// If page number is out of range, redirect to the last page.
if ($page > $totalPages) {
return $this->redirectToRoute('page_list', ['page' => $totalPages]);
}
$posts = $this->postRepository->getPaginatedPosts($page, $this->pageSize);
return $this->render('list_post/list.html.twig', [
'posts' => $posts,
'page' => $this->paginator->getVisiblePages($page, $totalPages, $this->maxVisiblePages),
]);
}
}

View File

@ -18,4 +18,20 @@ class PostRepository extends ServiceEntityRepository
{
parent::__construct($registry, Post::class);
}
/**
* @param int $page
* @param int $pageSize
* @return array<Post>
*/
public function getPaginatedPosts(int $page, int $pageSize): array
{
$query = $this->createQueryBuilder('p')
->orderBy('p.id', 'DESC')
->setFirstResult(($page - 1) * $pageSize)
->setMaxResults($pageSize)
->getQuery();
return $query->getResult();
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Utils\Paginator;
class Paginator
{
/**
* Determinate which pages are visible in the paginator
* @param int $page
* @param int $totalPages
* @param int $maxVisiblePages
* @return PaginatorResult
*/
public function getVisiblePages(int $page, int $totalPages, int $maxVisiblePages): PaginatorResult
{
$half = floor($maxVisiblePages / 2);
if ($page <= $half) {
$start = 1;
$end = min($totalPages, $maxVisiblePages);
} elseif ($page > $totalPages - $half) {
$start = max(1, $totalPages - $maxVisiblePages + 1);
$end = $totalPages;
} else {
$start = $page - $half;
$end = min($totalPages, $page + $half);
}
/**
* @var array<int> $range
*/
$range = range($start, $end);
return new PaginatorResult(
$page,
$start == 1,
$end == $totalPages,
$totalPages,
$totalPages,
$range
);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Utils\Paginator;
readonly class PaginatorResult
{
public function __construct(
public int $currentPage,
public bool $firstPageVisible,
public bool $lastPageVisible,
public int $totalPages,
public int $lastPage,
/**
* @var array<int>
*/
public array $visiblePages,
) {
}
}

View File

@ -64,21 +64,6 @@
"phpcs.xml.dist"
]
},
"symfony/asset-mapper": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "6c28c471640cc2c6e60812ebcb961c526ef8997f"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/asset_mapper.yaml",
"importmap.php"
]
},
"symfony/console": {
"version": "7.1",
"recipe": {
@ -244,20 +229,6 @@
"config/routes/security.yaml"
]
},
"symfony/stimulus-bundle": {
"version": "2.18",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.13",
"ref": "6acd9ff4f7fd5626d2962109bd4ebab351d43c43"
},
"files": [
"assets/bootstrap.js",
"assets/controllers.json",
"assets/controllers/hello_controller.js"
]
},
"symfony/translation": {
"version": "7.1",
"recipe": {
@ -284,9 +255,6 @@
"templates/base.html.twig"
]
},
"symfony/ux-turbo": {
"version": "v2.18.0"
},
"symfony/validator": {
"version": "7.1",
"recipe": {
@ -312,6 +280,22 @@
"config/routes/web_profiler.yaml"
]
},
"symfony/webpack-encore-bundle": {
"version": "2.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.0",
"ref": "082d754b3bd54b3fc669f278f1eea955cfd23cf5"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/webpack_encore.yaml",
"package.json",
"webpack.config.js"
]
},
"twig/extra-bundle": {
"version": "v3.10.0"
}

View File

@ -2,16 +2,20 @@
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<title>{% block title %}Post App{% 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 %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% include 'header.html.twig' %}
<div class="container container-lg">
{% block body %}{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,5 @@
<div class="container">
<header class="d-flex justify-content-center py-3">
<h1>{{ 'List App' | trans }}</h1>
</header>
</div>

View File

@ -0,0 +1,36 @@
{% extends 'base.html.twig' %}
{% block body %}
{% for post in posts %}
<div class="card t-post-detail">
<div class="card-header">
<a class="t-detail-a" href="{{ path('detail_post', {postId: post.id}) }}">#{{ post.id }} - <span class="t-post-title">{{ post.title|u.truncate(80, '...', true)}}</span></a>
</div>
<div class="card-body">
<p class="card-text t-post-body">
{{ post.body|u.truncate(80, '...', true) }}
</p>
</div>
</div>
{% endfor %}
<nav aria-label="Page navigation">
<ul class="pagination t-pagination">
{% if not page.firstPageVisible %}
<li class="page-item t-page-item"><a class="page-link t-page-link" href="{{ path('page_list', {page: 1}) }}">1&mldr;</a></li>
{% endif %}
{% for i in page.visiblePages %}
<li class="page-item {% if i == page.currentPage %}active{% endif %} t-page-item">
{% if i == page.currentPage %}
<span class="page-link">{{ i }}</span>
{% else %}
<a class="page-link t-page-link" href="{{ path('page_list', {page: i}) }}">{{ i }}</a>
{% endif %}
</li>
{% endfor %}
{% if not page.lastPageVisible %}
<li class="page-item t-page-item"><a class="page-link t-page-link" href="{{ path('page_list', {page: page.lastPage}) }}">&mldr;{{ page.lastPage }}</a></li>
{% endif %}
</ul>
</nav>
{% endblock %}

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Tests\Web;
use App\Entity\Database\Posts\Post;
use App\Tests\Common\DatabaseTestTrait;
use App\Tests\Common\FakerTrait;
use App\Tests\Common\Generators\PostGeneratorTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\AbstractBrowser;
class ListPostControllerTest extends WebTestCase
{
use DatabaseTestTrait;
use FakerTrait;
use PostGeneratorTrait;
protected AbstractBrowser $client;
protected function setUp(): void
{
$this->client = static::createClient();
$this->bootDatabase();
$this->bootFaker();
$this->getFaker()->unique();
}
public function testPostListed(): void
{
$posts = $this->createTestPosts();
$crawler = $this->client->request('GET', '/posts/1');
$this->assertResponseIsSuccessful();
$this->assertSelectorCount(3, '.t-detail-a'); // 3 posts are listed
$displayedPosts = array_reverse(array_slice($posts, count($posts) - 3, 3)); // in ORDER by ID DESC
/**
* @var Post $post
*/
foreach ($displayedPosts as $i => $post) { // check texts
$this->assertSelectorTextContains('.t-post-detail:nth-child(' . ($i + 1) . ') .t-post-title', $this->truncateToWholeWords($post->title, 70));
$text = $crawler->filter('.t-post-detail:nth-child(' . ($i + 1) . ') .t-post-title')->text();
$this->assertLessThan(83, strlen($text), "Text $text too long"); // 80 + 3 extra characters for ellipsis
$this->assertSelectorTextContains('.t-post-detail:nth-child(' . ($i + 1) . ') .t-post-body', $this->truncateToWholeWords($post->body, 70));
$text = $crawler->filter('.t-post-detail:nth-child(' . ($i + 1) . ') .t-post-body')->text();
$this->assertLessThan(83, strlen($text), "Text $text too long"); // 80 + 3 extra characters for ellipsis
}
}
public function testPagination(): void
{
$posts = $this->createTestPosts();
$crawler = $this->client->request('GET', '/posts/1');
$this->assertResponseIsSuccessful();
$this->assertSelectorCount(4 /* 3 -> normal, 1 -> last page */, '.t-pagination .t-page-item');
$crawler = $this->client->click($crawler->filter('.t-pagination .t-page-item:nth-child(3) .t-page-link')->link());
$this->assertSelectorCount(5 /* 3 -> normal, 2 -> first page, last page */, '.t-pagination .t-page-item');
}
public function testGoToNonExistentPage(): void
{
$this->createTestPosts();
$this->client->request('GET', '/posts/999999');
$this->assertResponseStatusCodeSame(302);
$this->assertResponseHeaderSame('Location', '/posts/10' /* last page (30 posts /3 per page) */);
}
public function testGoToPageZero(): void
{
$this->createTestPosts();
$this->client->request('GET', '/posts/0');
$this->assertResponseStatusCodeSame(302);
$this->assertResponseHeaderSame('Location', '/posts' /* go to first page */);
}
public function testGoToDefault(): void
{
$this->createTestPosts();
$this->client->request('GET', '/');
$this->assertResponseIsSuccessful();
$this->assertSelectorCount(3, '.t-detail-a'); // 3 posts are listed
}
private function truncateToWholeWords(string $string, int $length): string
{
if (strlen($string) <= $length) {
return $string;
}
// Find the last space within the allowed length
$truncated = substr($string, 0, $length);
$lastSpace = strrpos($truncated, ' ');
// If there's no space, truncate to the exact length
if ($lastSpace === false) {
return substr($truncated, 0, $length);
}
return substr($truncated, 0, $lastSpace);
}
/**
* @return Post[]
*/
private function createTestPosts(): array
{
$posts = [];
for ($i = 1; $i <= 30; $i++) {
$posts[] = $this->createPost($i);
}
return $posts;
}
}

73
webpack.config.js Normal file
View File

@ -0,0 +1,73 @@
const Encore = require('@symfony/webpack-encore');
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
Encore
// directory where compiled assets will be stored
.setOutputPath('public/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
// only needed for CDN's or subdirectory deploy
//.setManifestKeyPrefix('build/')
/*
* ENTRY CONFIG
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/
.addEntry('app', './assets/app.js')
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()
/*
* FEATURE CONFIG
*
* Enable & configure other features below. For a full
* list of features, see:
* https://symfony.com/doc/current/frontend.html#adding-more-features
*/
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// configure Babel
// .configureBabel((config) => {
// config.plugins.push('@babel/a-babel-plugin');
// })
// enables and configure @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = '3.23';
})
// enables Sass/SCSS support
.enableSassLoader()
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment if you use React
//.enableReactPreset()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes(Encore.isProduction())
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
;
module.exports = Encore.getWebpackConfig();