init
This commit is contained in:
commit
d36edf88cf
32
.env
Normal file
32
.env
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# 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=
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/mailer ###
|
||||||
|
MAILER_DSN=smtp://localhost
|
||||||
|
###< symfony/mailer ###
|
||||||
|
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||||
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||||
|
#
|
||||||
|
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||||
|
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
|
||||||
|
# DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
6
.env.test
Normal file
6
.env.test
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# define your env variables for the test env here
|
||||||
|
KERNEL_CLASS='App\Kernel'
|
||||||
|
APP_SECRET='$ecretf0rt3st'
|
||||||
|
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||||
|
PANTHER_APP_ENV=panther
|
||||||
|
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
19
.eslintrc.json
Normal file
19
.eslintrc.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/essential",
|
||||||
|
"standard"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
}
|
||||||
|
}
|
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
/.env.local
|
||||||
|
/.env.local.php
|
||||||
|
/.env.*.local
|
||||||
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
|
/public/bundles/
|
||||||
|
/public/js/
|
||||||
|
/src/Command/TestCommand.php
|
||||||
|
/var/
|
||||||
|
/vendor/
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/phpunit-bridge ###
|
||||||
|
.phpunit
|
||||||
|
.phpunit.result.cache
|
||||||
|
/phpunit.xml
|
||||||
|
###< symfony/phpunit-bridge ###
|
||||||
|
|
||||||
|
###> symfony/webpack-encore-bundle ###
|
||||||
|
/node_modules/
|
||||||
|
/public/build/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
###< symfony/webpack-encore-bundle ###
|
||||||
|
|
||||||
|
/public/uploads/*
|
||||||
|
!/public/uploads/.gitkeep
|
||||||
|
/public/media/
|
||||||
|
/migrations/*
|
||||||
|
!/migrations/.gitkeep
|
1
.php-version
Normal file
1
.php-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
8.0
|
97
CHANGELOG.md
Normal file
97
CHANGELOG.md
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
### Fixed
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
## [1.7.2] - 2022-03-03
|
||||||
|
### Added
|
||||||
|
* add templates to render sections and items in the admin menu
|
||||||
|
### Fixed
|
||||||
|
* fix the analytic table when a path is a long
|
||||||
|
|
||||||
|
## [1.7.1] - 2022-03-01
|
||||||
|
### Added
|
||||||
|
* add translations
|
||||||
|
### Fixed
|
||||||
|
* fix missing directories
|
||||||
|
|
||||||
|
## [1.7.0] - 2022-03-01
|
||||||
|
### Fixed
|
||||||
|
* fix the analytic referers table when a referer has a long domain
|
||||||
|
### Changed
|
||||||
|
* upgrade dependencies
|
||||||
|
* move assets to the core directory
|
||||||
|
|
||||||
|
## [1.6.0] - 2022-02-28
|
||||||
|
### Added
|
||||||
|
* add block in field templates to allow override
|
||||||
|
* merge route params in crud admin redirects
|
||||||
|
* improve murph:user:create command
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* fix form namespace prefix in the crud controller maker
|
||||||
|
* fix date field when the value is empty
|
||||||
|
* fix crud batch column width
|
||||||
|
* fix sidebar icon width
|
||||||
|
* fix cache clear task
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* remove password generation from the user factory
|
||||||
|
|
||||||
|
## [1.5.0] - 2022-02-25
|
||||||
|
### Added
|
||||||
|
* add desktop views and mobile views
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* upgrade dependencies
|
||||||
|
* replace jaybizzle/crawler-detect with matomo/device-detector
|
||||||
|
|
||||||
|
## [1.4.1] - 2022-02-23
|
||||||
|
### Added
|
||||||
|
* handle app urls in twig routing filters
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* fix views in analytics modal
|
||||||
|
* replace empty path with "/" in analytics
|
||||||
|
### Changed
|
||||||
|
* update default templates
|
||||||
|
|
||||||
|
## [1.4.0] - 2022-02-21
|
||||||
|
### Added
|
||||||
|
* add basic analytics
|
||||||
|
|
||||||
|
## [1.3.0] - 2022-02-19
|
||||||
|
### Added
|
||||||
|
* add support of regexp with substitution in redirect
|
||||||
|
* url tags can be used as redirect location
|
||||||
|
* add builders to replace file information tags and url tags
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* fix filemanager sorting
|
||||||
|
* fix batch action setter
|
||||||
|
|
||||||
|
## [1.2.0] - 2022-02-14
|
||||||
|
### Added
|
||||||
|
* add sort in file manager
|
||||||
|
* add redirect manager
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* replace node-sass with sass
|
||||||
|
|
||||||
|
## [1.1.0] - 2022-02-29
|
||||||
|
### Added
|
||||||
|
* add directory upload in file manager
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* fix admin node routing
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* symfony/swiftmailer-bundle is replaced by symfony/mailer
|
||||||
|
|
||||||
|
## [1.0.1] - 2022-02-25
|
||||||
|
### Fixed
|
||||||
|
* fix Makefile environment vars (renaming)
|
||||||
|
* fix composer minimum stability
|
||||||
|
|
||||||
|
## [1.0.0] - 2022-01-23
|
27
Makefile
Normal file
27
Makefile
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
COMPOSER_BIN ?= composer
|
||||||
|
PHP_BIN ?= php8.0
|
||||||
|
SSH_BIN ?= ssh
|
||||||
|
YARN_BIN ?= yarn
|
||||||
|
NPM_BIN ?= npm
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
asset-watch:
|
||||||
|
$(YARN_BIN)
|
||||||
|
$(NPM_BIN) run watch
|
||||||
|
|
||||||
|
asset: js-routing
|
||||||
|
$(YARN_BIN)
|
||||||
|
$(NPM_BIN) run build
|
||||||
|
|
||||||
|
js-routing: doctrine-migration
|
||||||
|
$(PHP_BIN) bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -fr var/cache/dev/*
|
||||||
|
rm -fr var/cache/prod/*
|
||||||
|
|
||||||
|
doctrine-migration:
|
||||||
|
PHP=$(PHP_BIN) ./bin/doctrine-migrate
|
||||||
|
|
||||||
|
build: clean js-routing asset
|
16
README.md
Normal file
16
README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Murph
|
||||||
|
|
||||||
|
Muprh is an **open-source CMF** built on top of Symfony that helps you to **build your own CMS with several domains and languages**. It comes with:
|
||||||
|
|
||||||
|
* A fully implemented and customizable **tree manager** 🌳
|
||||||
|
* A **CRUD generator** ✏️
|
||||||
|
* A global **settings manager** and a navigation settings manager ⚙️
|
||||||
|
* A **tasks manager** 🧹
|
||||||
|
* A basic **web analytics** 📊
|
||||||
|
* **2FA authentication** 🔒
|
||||||
|
|
||||||
|
**Symfony developers will love build on Murph 🧪**
|
||||||
|
|
||||||
|
**End users will be fond of the interface and the powerful tools 💜**
|
||||||
|
|
||||||
|
📗 [Read the documentation](https://doc.murph-project.org/)
|
53
UPGRADE.md
Normal file
53
UPGRADE.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## Upgrade to v1.7.0
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn add sortablejs@^1.14.0
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
* `assets/css/_admin_extend.scss` is removed
|
||||||
|
* `assets/css/_admin_vars.scss` is removed
|
||||||
|
* `assets/css/_admin_vars.scss` is changed
|
||||||
|
* `assets/js/admin` is removed
|
||||||
|
* `assets/js/admin.js` is changed
|
||||||
|
|
||||||
|
|
||||||
|
## Upgrade to v1.5.0
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
composer remove jaybizzle/crawler-detect
|
||||||
|
composer require matomo/device-detector
|
||||||
|
make doctrine-migration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upgrade to v1.4.0
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn remove node-sass
|
||||||
|
yarn add sass --dev --save
|
||||||
|
yarn add chart.js --save
|
||||||
|
composer require jaybizzle/crawler-detect
|
||||||
|
make doctrine-migration
|
||||||
|
make asset
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```
|
||||||
|
// config/services.yaml
|
||||||
|
services:
|
||||||
|
App\Core\EventListener\RedirectListener:
|
||||||
|
tags:
|
||||||
|
- { name: kernel.event_listener, event: kernel.exception }
|
||||||
|
|
||||||
|
App\Core\EventListener\AnalyticListener:
|
||||||
|
tags:
|
||||||
|
- { name: kernel.event_listener, event: kernel.request }
|
||||||
|
```
|
6
assets/css/admin.scss
Normal file
6
assets/css/admin.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/* Custom variables */
|
||||||
|
|
||||||
|
@import "../../core/Resources/assets/css/admin.scss";
|
||||||
|
|
||||||
|
/* Custom CSS */
|
||||||
|
|
2
assets/css/app.scss
Normal file
2
assets/css/app.scss
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/* CSS of you app */
|
||||||
|
|
92
assets/images/core/logo.svg
Normal file
92
assets/images/core/logo.svg
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="92.5"
|
||||||
|
height="92.500008"
|
||||||
|
viewBox="0 0 24.473958 24.473961"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2782"
|
||||||
|
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||||
|
sodipodi:docname="logo.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2776" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="3.959798"
|
||||||
|
inkscape:cx="144.24896"
|
||||||
|
inkscape:cy="62.558177"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="41"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2779">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Calque 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-93.596354,-136.59635)">
|
||||||
|
<g
|
||||||
|
transform="translate(14.977383,9.0140333)"
|
||||||
|
id="g2760">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="rect2455"
|
||||||
|
d="m 80.981321,127.58232 h 19.749259 c 1.30874,0 2.36235,1.05361 2.36235,2.36235 v 19.74926 c 0,1.30874 -1.05361,2.36235 -2.36235,2.36235 H 80.981321 c -1.30874,0 -2.36235,-1.05361 -2.36235,-2.36235 v -19.74926 c 0,-1.30874 1.05361,-2.36235 2.36235,-2.36235 z"
|
||||||
|
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#1e2430;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.58333302;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;paint-order:stroke fill markers;enable-background:accumulate" />
|
||||||
|
<g
|
||||||
|
transform="translate(-28.224115,84.535074)"
|
||||||
|
id="text2474-5"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:24.23528671px;line-height:125%;font-family:Tahoma;-inkscape-font-specification:'Tahoma, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
aria-label="M">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path2522"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Trebuchet MS';-inkscape-font-specification:'Trebuchet MS';fill:#ffcc00;fill-opacity:1;stroke-width:0.26458332px"
|
||||||
|
d="m 125.90001,62.475697 h -2.98208 l -1.79871,-9.348573 -3.49093,9.573412 h -1.10052 l -3.49093,-9.573412 -1.86971,9.348573 h -2.97024 l 3.49092,-17.34811 h 1.63304 l 3.75126,11.679798 3.66843,-11.679798 h 1.62121 z" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="rect2455-3-6"
|
||||||
|
d="m 102.39375,128.26496 -23.092649,23.09213 c 0.427643,0.43204 1.021784,0.69919 1.680519,0.69919 h 19.74918 c 1.30874,0 2.36213,-1.05339 2.36213,-2.36213 v -19.74918 c 0,-0.65861 -0.26729,-1.25238 -0.69918,-1.68001 z"
|
||||||
|
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;vector-effect:none;fill:#19b4db;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.58333302;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;paint-order:stroke fill markers;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path2525-9-7"
|
||||||
|
d="m 94.723251,132.53384 -4.531505,4.53151 -1.343589,4.27726 -0.713134,-2.22054 -2.044319,2.04432 2.213301,6.0694 h 1.10019 l 3.491262,-9.57306 1.798338,9.34826 h 2.982249 z m -11.105782,11.10527 -3.371887,3.37188 h 2.697509 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:24.23528481px;line-height:125%;font-family:'Trebuchet MS';-inkscape-font-specification:'Trebuchet MS';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#1e2430;fill-opacity:1;stroke:#1e2430;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.3 KiB |
1
assets/js/admin.js
Normal file
1
assets/js/admin.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import '../../core/Resources/assets/js/admin.js'
|
1
assets/js/app.js
Normal file
1
assets/js/app.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import '../css/app.scss'
|
43
bin/console
Executable file
43
bin/console
Executable file
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Dotenv\Dotenv;
|
||||||
|
use Symfony\Component\ErrorHandler\Debug;
|
||||||
|
|
||||||
|
if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
|
||||||
|
echo 'Warning: The console should be invoked via the CLI version of PHP, not the '.PHP_SAPI.' SAPI'.PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_time_limit(0);
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (!class_exists(Application::class) || !class_exists(Dotenv::class)) {
|
||||||
|
throw new LogicException('You need to add "symfony/framework-bundle" and "symfony/dotenv" as Composer dependencies.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = new ArgvInput();
|
||||||
|
if (null !== $env = $input->getParameterOption(['--env', '-e'], null, true)) {
|
||||||
|
putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input->hasParameterOption('--no-debug', true)) {
|
||||||
|
putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||||
|
|
||||||
|
if ($_SERVER['APP_DEBUG']) {
|
||||||
|
umask(0000);
|
||||||
|
|
||||||
|
if (class_exists(Debug::class)) {
|
||||||
|
Debug::enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||||
|
$application = new Application($kernel);
|
||||||
|
$application->run($input);
|
7
bin/doctrine-migrate
Executable file
7
bin/doctrine-migrate
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
CLASS_NAME="$(echo "yes" | "$PHP" ./bin/console doctrine:migration:diff -e dev | grep -o "Version[0-9]*" | tail -n 1)"
|
||||||
|
|
||||||
|
if [ -n "$CLASS_NAME" ]; then
|
||||||
|
echo "yes" | "$PHP" ./bin/console doctrine:migration:exec --up "DoctrineMigrations\\$CLASS_NAME" -e dev
|
||||||
|
fi
|
13
bin/phpunit
Executable file
13
bin/phpunit
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if (!file_exists(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||||
|
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === getenv('SYMFONY_PHPUNIT_DIR')) {
|
||||||
|
putenv('SYMFONY_PHPUNIT_DIR='.__DIR__.'/.phpunit');
|
||||||
|
}
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
|
121
composer.json
Normal file
121
composer.json
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
{
|
||||||
|
"name": "murph/murph-skeleton",
|
||||||
|
"description": "A powerful CMS framework",
|
||||||
|
"type": "project",
|
||||||
|
"license": "MIT",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"bjeavons/zxcvbn-php": "^1.3",
|
||||||
|
"cocur/slugify": "^4.1",
|
||||||
|
"composer/package-versions-deprecated": "1.11.99.1",
|
||||||
|
"doctrine/annotations": "^1.0",
|
||||||
|
"doctrine/doctrine-bundle": "^2.5",
|
||||||
|
"doctrine/doctrine-migrations-bundle": "^3.2",
|
||||||
|
"doctrine/orm": "^2.11",
|
||||||
|
"friendsofsymfony/jsrouting-bundle": "^2.8",
|
||||||
|
"jaybizzle/crawler-detect": "^1.2",
|
||||||
|
"knplabs/doctrine-behaviors": "^2.6",
|
||||||
|
"knplabs/knp-paginator-bundle": "^5.8",
|
||||||
|
"liip/imagine-bundle": "^2.7",
|
||||||
|
"matomo/device-detector": "^5.0",
|
||||||
|
"phpdocumentor/reflection-docblock": "^5.3",
|
||||||
|
"scheb/2fa-google-authenticator": "^5.13",
|
||||||
|
"scheb/2fa-qr-code": "^5.13",
|
||||||
|
"sensio/framework-extra-bundle": "^6.2",
|
||||||
|
"sensiolabs/ansi-to-html": "^1.2",
|
||||||
|
"spe/filesize-extension-bundle": "~2.0.0",
|
||||||
|
"stof/doctrine-extensions-bundle": "^1.7",
|
||||||
|
"symfony/apache-pack": "^1.0",
|
||||||
|
"symfony/asset": "5.4.*",
|
||||||
|
"symfony/console": "5.4.*",
|
||||||
|
"symfony/dotenv": "5.4.*",
|
||||||
|
"symfony/event-dispatcher": "5.4.*",
|
||||||
|
"symfony/expression-language": "5.4.*",
|
||||||
|
"symfony/finder": "5.4.*",
|
||||||
|
"symfony/flex": "^1.3.1",
|
||||||
|
"symfony/form": "5.4.*",
|
||||||
|
"symfony/framework-bundle": "5.4.*",
|
||||||
|
"symfony/http-client": "5.4.*",
|
||||||
|
"symfony/intl": "5.4.*",
|
||||||
|
"symfony/mailer": "5.4.*",
|
||||||
|
"symfony/mime": "5.4.*",
|
||||||
|
"symfony/monolog-bundle": "^3.1",
|
||||||
|
"symfony/notifier": "5.4.*",
|
||||||
|
"symfony/process": "5.4.*",
|
||||||
|
"symfony/property-access": "5.4.*",
|
||||||
|
"symfony/property-info": "5.4.*",
|
||||||
|
"symfony/proxy-manager-bridge": "5.4.*",
|
||||||
|
"symfony/security-bundle": "5.4.*",
|
||||||
|
"symfony/serializer": "5.4.*",
|
||||||
|
"symfony/string": "5.4.*",
|
||||||
|
"symfony/translation": "5.4.*",
|
||||||
|
"symfony/twig-bundle": "^5.2",
|
||||||
|
"symfony/validator": "5.4.*",
|
||||||
|
"symfony/web-link": "5.4.*",
|
||||||
|
"symfony/webpack-encore-bundle": "^1.11",
|
||||||
|
"symfony/yaml": "5.4.*",
|
||||||
|
"twig/extra-bundle": "^2.12|^3.3",
|
||||||
|
"twig/twig": "^2.12|^3.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/browser-kit": "^5.4",
|
||||||
|
"symfony/css-selector": "^5.4",
|
||||||
|
"symfony/debug-bundle": "^5.4",
|
||||||
|
"symfony/maker-bundle": "^1.0",
|
||||||
|
"symfony/phpunit-bridge": "^5.4",
|
||||||
|
"symfony/stopwatch": "^5.4",
|
||||||
|
"symfony/var-dumper": "^5.4",
|
||||||
|
"symfony/web-profiler-bundle": "^5.4"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": {
|
||||||
|
"*": "dist"
|
||||||
|
},
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"symfony/flex": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/",
|
||||||
|
"App\\Core\\": "core/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"symfony/polyfill-ctype": "*",
|
||||||
|
"symfony/polyfill-iconv": "*",
|
||||||
|
"symfony/polyfill-php72": "*"
|
||||||
|
},
|
||||||
|
"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": "5.4.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
config/bundles.php
Normal file
25
config/bundles.php
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
|
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
|
||||||
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
|
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||||
|
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||||
|
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
||||||
|
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
|
||||||
|
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
|
||||||
|
Knp\DoctrineBehaviors\DoctrineBehaviorsBundle::class => ['all' => true],
|
||||||
|
FOS\JsRoutingBundle\FOSJsRoutingBundle::class => ['all' => true],
|
||||||
|
SPE\FilesizeExtensionBundle\SPEFilesizeExtensionBundle::class => ['all' => true],
|
||||||
|
Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true],
|
||||||
|
App\Core\Bundle\CoreBundle::class => ['all' => true],
|
||||||
|
App\Bundle\AppBundle::class => ['all' => true],
|
||||||
|
];
|
6
config/packages/ansi_to_html.yaml
Normal file
6
config/packages/ansi_to_html.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
services:
|
||||||
|
_defaults:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
|
||||||
|
SensioLabs\AnsiConverter\Bridge\Twig\AnsiExtension: null
|
42
config/packages/app.yaml
Normal file
42
config/packages/app.yaml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
core:
|
||||||
|
site:
|
||||||
|
name: "Murph"
|
||||||
|
logo: "build/images/core/logo.svg"
|
||||||
|
# controllers:
|
||||||
|
# - {name: 'Foo', action: 'App\Controller\ExampleController::foo'}
|
||||||
|
pages:
|
||||||
|
App\Entity\Page\SimplePage:
|
||||||
|
name: 'Simple page'
|
||||||
|
templates:
|
||||||
|
- {name: "Default", file: "page/simple/default.html.twig"}
|
||||||
|
file_manager:
|
||||||
|
# mimes:
|
||||||
|
# - image/png
|
||||||
|
# - image/jpg
|
||||||
|
# - image/jpeg
|
||||||
|
# - image/gif
|
||||||
|
# - image/svg+xml
|
||||||
|
# - video/mp4
|
||||||
|
# - audio/mpeg3
|
||||||
|
# - audio/x-mpeg-3
|
||||||
|
# - multipart/x-zip
|
||||||
|
# - multipart/x-gzip
|
||||||
|
# - application/pdf
|
||||||
|
# - application/ogg
|
||||||
|
# - application/zip
|
||||||
|
# - application/rar
|
||||||
|
# - application/x-rar-compressed
|
||||||
|
# - application/x-zip-compressed
|
||||||
|
# - application/tar
|
||||||
|
# - application/x-tar
|
||||||
|
# - application/x-bzip
|
||||||
|
# - application/x-bzip2
|
||||||
|
# - application/x-gzip
|
||||||
|
# - application/octet-stream
|
||||||
|
# - application/msword
|
||||||
|
# - text/plain
|
||||||
|
# - text/css
|
||||||
|
# path: "%kernel.project_dir%/public/uploads"
|
||||||
|
# path_uri: "/uploads"
|
||||||
|
# path_locked:
|
||||||
|
# - "%kernel.project_dir%/public/uploads"
|
3
config/packages/assets.yaml
Normal file
3
config/packages/assets.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
framework:
|
||||||
|
assets:
|
||||||
|
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal 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
|
4
config/packages/dev/debug.yaml
Normal file
4
config/packages/dev/debug.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
debug:
|
||||||
|
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||||
|
# See the "server:dump" command to start a new server.
|
||||||
|
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
19
config/packages/dev/monolog.yaml
Normal file
19
config/packages/dev/monolog.yaml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
channels: ["!event"]
|
||||||
|
# uncomment to get logging in your browser
|
||||||
|
# you may have to allow bigger header sizes in your Web server configuration
|
||||||
|
#firephp:
|
||||||
|
# type: firephp
|
||||||
|
# level: info
|
||||||
|
#chromephp:
|
||||||
|
# type: chromephp
|
||||||
|
# level: info
|
||||||
|
console:
|
||||||
|
type: console
|
||||||
|
process_psr_3_messages: false
|
||||||
|
channels: ["!event", "!doctrine", "!console"]
|
6
config/packages/dev/web_profiler.yaml
Normal file
6
config/packages/dev/web_profiler.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
web_profiler:
|
||||||
|
toolbar: true
|
||||||
|
intercept_redirects: false
|
||||||
|
|
||||||
|
framework:
|
||||||
|
profiler: { only_exceptions: false }
|
30
config/packages/doctrine.yaml
Normal file
30
config/packages/doctrine.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
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: '13'
|
||||||
|
orm:
|
||||||
|
auto_generate_proxy_classes: true
|
||||||
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
|
auto_mapping: true
|
||||||
|
mappings:
|
||||||
|
App\Core\Entity:
|
||||||
|
is_bundle: false
|
||||||
|
type: annotation
|
||||||
|
dir: '%kernel.project_dir%/core/Entity'
|
||||||
|
prefix: 'App\Core\Entity'
|
||||||
|
alias: App\Core\Entity
|
||||||
|
App\Entity:
|
||||||
|
is_bundle: false
|
||||||
|
type: annotation
|
||||||
|
dir: '%kernel.project_dir%/src/Entity'
|
||||||
|
prefix: 'App\Entity'
|
||||||
|
alias: App\Entity
|
||||||
|
gedmo_tree:
|
||||||
|
type: annotation
|
||||||
|
prefix: Gedmo\Tree\Entity
|
||||||
|
dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Tree/Entity"
|
||||||
|
alias: GedmoTree # (optional) it will default to the name set for the mapping
|
||||||
|
is_bundle: false
|
5
config/packages/doctrine_migrations.yaml
Normal file
5
config/packages/doctrine_migrations.yaml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
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'
|
3
config/packages/fos_js_routing.yaml
Normal file
3
config/packages/fos_js_routing.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fos_js_routing:
|
||||||
|
routes_to_expose:
|
||||||
|
- liip_imagine_filter
|
17
config/packages/framework.yaml
Normal file
17
config/packages/framework.yaml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
|
framework:
|
||||||
|
secret: '%env(APP_SECRET)%'
|
||||||
|
#csrf_protection: true
|
||||||
|
#http_method_override: true
|
||||||
|
|
||||||
|
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||||
|
# Remove or comment this section to explicitly disable session support.
|
||||||
|
session:
|
||||||
|
handler_id: null
|
||||||
|
cookie_secure: auto
|
||||||
|
cookie_samesite: lax
|
||||||
|
|
||||||
|
#esi: true
|
||||||
|
#fragments: true
|
||||||
|
php_errors:
|
||||||
|
log: true
|
11
config/packages/knp.yaml
Normal file
11
config/packages/knp.yaml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
knp_paginator:
|
||||||
|
page_range: 4
|
||||||
|
default_options:
|
||||||
|
page_name: page # page query parameter name
|
||||||
|
sort_field_name: sort # sort field query parameter name
|
||||||
|
sort_direction_name: direction # sort direction query parameter name
|
||||||
|
distinct: true # ensure distinct results, useful when ORM queries are using GROUP BY statements
|
||||||
|
template:
|
||||||
|
pagination: '@Core/pager/sliding.html.twig' # sliding pagination controls template
|
||||||
|
sortable: '@KnpPaginator/Pagination/sortable_link.html.twig' # sort link template
|
||||||
|
filtration: '@KnpPaginator/Pagination/filtration.html.twig' # filters template
|
9
config/packages/liip_imagine.yaml
Normal file
9
config/packages/liip_imagine.yaml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# See dos how to configure the bundle: https://symfony.com/doc/current/bundles/LiipImagineBundle/basic-usage.html
|
||||||
|
liip_imagine:
|
||||||
|
# valid drivers options include "gd" or "gmagick" or "imagick"
|
||||||
|
driver: "imagick"
|
||||||
|
filter_sets:
|
||||||
|
file_manager_thumbnail_filter:
|
||||||
|
filters:
|
||||||
|
downscale:
|
||||||
|
max: [120, 120]
|
5
config/packages/mailer.yaml
Normal file
5
config/packages/mailer.yaml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
framework:
|
||||||
|
mailer:
|
||||||
|
dsn: '%env(MAILER_DSN)%'
|
||||||
|
headers:
|
||||||
|
From: 'Example <example@localhost>'
|
16
config/packages/notifier.yaml
Normal file
16
config/packages/notifier.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
framework:
|
||||||
|
notifier:
|
||||||
|
#chatter_transports:
|
||||||
|
# slack: '%env(SLACK_DSN)%'
|
||||||
|
# telegram: '%env(TELEGRAM_DSN)%'
|
||||||
|
#texter_transports:
|
||||||
|
# twilio: '%env(TWILIO_DSN)%'
|
||||||
|
# nexmo: '%env(NEXMO_DSN)%'
|
||||||
|
channel_policy:
|
||||||
|
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
|
||||||
|
urgent: ['email']
|
||||||
|
high: ['email']
|
||||||
|
medium: ['email']
|
||||||
|
low: ['email']
|
||||||
|
admin_recipients:
|
||||||
|
- { email: admin@example.com }
|
8
config/packages/prod/deprecations.yaml
Normal file
8
config/packages/prod/deprecations.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||||
|
#monolog:
|
||||||
|
# channels: [deprecation]
|
||||||
|
# handlers:
|
||||||
|
# deprecation:
|
||||||
|
# type: stream
|
||||||
|
# channels: [deprecation]
|
||||||
|
# path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
|
20
config/packages/prod/doctrine.yaml
Normal file
20
config/packages/prod/doctrine.yaml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
doctrine:
|
||||||
|
orm:
|
||||||
|
auto_generate_proxy_classes: false
|
||||||
|
metadata_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.system_cache_pool
|
||||||
|
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
|
16
config/packages/prod/monolog.yaml
Normal file
16
config/packages/prod/monolog.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: fingers_crossed
|
||||||
|
action_level: error
|
||||||
|
handler: nested
|
||||||
|
excluded_http_codes: [404, 405]
|
||||||
|
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||||
|
nested:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
console:
|
||||||
|
type: console
|
||||||
|
process_psr_3_messages: false
|
||||||
|
channels: ["!event", "!doctrine"]
|
3
config/packages/prod/routing.yaml
Normal file
3
config/packages/prod/routing.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
framework:
|
||||||
|
router:
|
||||||
|
strict_requirements: null
|
4
config/packages/prod/webpack_encore.yaml
Normal file
4
config/packages/prod/webpack_encore.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#webpack_encore:
|
||||||
|
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||||
|
# Available in version 1.2
|
||||||
|
#cache: true
|
7
config/packages/routing.yaml
Normal file
7
config/packages/routing.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
framework:
|
||||||
|
router:
|
||||||
|
utf8: true
|
||||||
|
|
||||||
|
# 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
|
15
config/packages/scheb_2fa.yaml
Normal file
15
config/packages/scheb_2fa.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md
|
||||||
|
scheb_two_factor:
|
||||||
|
google:
|
||||||
|
enabled: true
|
||||||
|
issuer: "Muprh"
|
||||||
|
server_name:
|
||||||
|
digits: 6
|
||||||
|
window: 1
|
||||||
|
template: "@Core/auth/2fa.html.twig"
|
||||||
|
security_tokens:
|
||||||
|
- Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
|
||||||
|
# If you're using guard-based authentication, you have to use this one:
|
||||||
|
- Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
|
||||||
|
# If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one:
|
||||||
|
# - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
|
54
config/packages/security.yaml
Normal file
54
config/packages/security.yaml
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
security:
|
||||||
|
encoders:
|
||||||
|
App\Entity\User:
|
||||||
|
algorithm: auto
|
||||||
|
|
||||||
|
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
|
||||||
|
providers:
|
||||||
|
# used to reload user from session & other features (e.g. switch_user)
|
||||||
|
app_user_provider:
|
||||||
|
entity:
|
||||||
|
class: App\Entity\User
|
||||||
|
property: email
|
||||||
|
|
||||||
|
role_hierarchy:
|
||||||
|
ROLE_WRITER: ROLE_USER
|
||||||
|
ROLE_ADMIN: ROLE_WRITER
|
||||||
|
|
||||||
|
firewalls:
|
||||||
|
dev:
|
||||||
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
|
security: false
|
||||||
|
main:
|
||||||
|
anonymous: ~
|
||||||
|
two_factor:
|
||||||
|
auth_form_path: 2fa_login # The route name you have used in the routes.yaml
|
||||||
|
check_path: 2fa_login_check # The route name you have used in the routes.yaml
|
||||||
|
guard:
|
||||||
|
authenticators:
|
||||||
|
- App\Core\Authenticator\LoginFormAuthenticator
|
||||||
|
form_login:
|
||||||
|
login_path: auth_login
|
||||||
|
check_path: auth_login
|
||||||
|
csrf_token_generator: security.csrf.token_manager
|
||||||
|
logout:
|
||||||
|
path: auth_logout
|
||||||
|
target: /
|
||||||
|
remember_me:
|
||||||
|
secret: '%kernel.secret%'
|
||||||
|
lifetime: 604800
|
||||||
|
path: /
|
||||||
|
|
||||||
|
# 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: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
|
- { path: ^/resetting, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
|
- { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
|
||||||
|
- { path: ^/admin/user, roles: ROLE_ADMIN }
|
||||||
|
- { path: ^/admin/task, roles: ROLE_ADMIN }
|
||||||
|
- { path: ^/admin/setting, roles: ROLE_ADMIN }
|
||||||
|
- { path: ^/admin/site, roles: ROLE_WRITER }
|
||||||
|
- { path: ^/admin/file_manager, roles: ROLE_WRITER }
|
||||||
|
- { path: ^/admin, roles: ROLE_USER }
|
||||||
|
- { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
3
config/packages/sensio_framework_extra.yaml
Normal file
3
config/packages/sensio_framework_extra.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
sensio_framework_extra:
|
||||||
|
router:
|
||||||
|
annotations: false
|
4
config/packages/stof_doctrine_extensions.yaml
Normal file
4
config/packages/stof_doctrine_extensions.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html
|
||||||
|
# See the official DoctrineExtensions documentation for more details: https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc/
|
||||||
|
stof_doctrine_extensions:
|
||||||
|
default_locale: en_US
|
4
config/packages/test/framework.yaml
Normal file
4
config/packages/test/framework.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
framework:
|
||||||
|
test: true
|
||||||
|
session:
|
||||||
|
storage_id: session.storage.mock_file
|
12
config/packages/test/monolog.yaml
Normal file
12
config/packages/test/monolog.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: fingers_crossed
|
||||||
|
action_level: error
|
||||||
|
handler: nested
|
||||||
|
excluded_http_codes: [404, 405]
|
||||||
|
channels: ["!event"]
|
||||||
|
nested:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
2
config/packages/test/twig.yaml
Normal file
2
config/packages/test/twig.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
twig:
|
||||||
|
strict_variables: true
|
3
config/packages/test/validator.yaml
Normal file
3
config/packages/test/validator.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
not_compromised_password: false
|
6
config/packages/test/web_profiler.yaml
Normal file
6
config/packages/test/web_profiler.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
web_profiler:
|
||||||
|
toolbar: false
|
||||||
|
intercept_redirects: false
|
||||||
|
|
||||||
|
framework:
|
||||||
|
profiler: { collect: false }
|
2
config/packages/test/webpack_encore.yaml
Normal file
2
config/packages/test/webpack_encore.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
#webpack_encore:
|
||||||
|
# strict_mode: false
|
8
config/packages/translation.yaml
Normal file
8
config/packages/translation.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
framework:
|
||||||
|
default_locale: fr
|
||||||
|
translator:
|
||||||
|
default_path: '%kernel.project_dir%/translations'
|
||||||
|
paths:
|
||||||
|
- '%kernel.project_dir%/core/Resources/translations'
|
||||||
|
fallbacks:
|
||||||
|
- en
|
6
config/packages/twig.yaml
Normal file
6
config/packages/twig.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
twig:
|
||||||
|
default_path: '%kernel.project_dir%/templates'
|
||||||
|
form_themes: ['@Core/form/bootstrap_4_form_theme.html.twig']
|
||||||
|
paths:
|
||||||
|
'%kernel.project_dir%/templates/core/': Core
|
||||||
|
'%kernel.project_dir%/core/Resources/views/': Core
|
8
config/packages/validator.yaml
Normal file
8
config/packages/validator.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
email_validation_mode: html5
|
||||||
|
|
||||||
|
# Enables validator auto-mapping support.
|
||||||
|
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||||
|
#auto_mapping:
|
||||||
|
# App\Entity\: []
|
30
config/packages/webpack_encore.yaml
Normal file
30
config/packages/webpack_encore.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
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
|
||||||
|
# link_attributes:
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
# pass "frontend" as the 3rg arg to the Twig functions
|
||||||
|
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||||
|
|
||||||
|
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||||
|
|
||||||
|
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||||
|
# Put in config/packages/prod/webpack_encore.yaml
|
||||||
|
# cache: true
|
5
config/preload.php
Normal file
5
config/preload.php
Normal 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';
|
||||||
|
}
|
15
config/routes.yaml
Normal file
15
config/routes.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#index:
|
||||||
|
# path: /
|
||||||
|
# controller: App\Controller\DefaultController::index
|
||||||
|
|
||||||
|
site_route:
|
||||||
|
resource: 'site.route_loader::loadRoutes'
|
||||||
|
type: extra
|
||||||
|
|
||||||
|
2fa_login:
|
||||||
|
path: /2fa
|
||||||
|
defaults:
|
||||||
|
_controller: "scheb_two_factor.form_controller:form"
|
||||||
|
|
||||||
|
2fa_login_check:
|
||||||
|
path: /2fa_check
|
11
config/routes/annotations.yaml
Normal file
11
config/routes/annotations.yaml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
controllers:
|
||||||
|
resource: ../../src/Controller/
|
||||||
|
type: annotation
|
||||||
|
|
||||||
|
core_controllers:
|
||||||
|
resource: ../../core/Controller/
|
||||||
|
type: annotation
|
||||||
|
|
||||||
|
kernel:
|
||||||
|
resource: ../../src/Kernel.php
|
||||||
|
type: annotation
|
3
config/routes/dev/framework.yaml
Normal file
3
config/routes/dev/framework.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
_errors:
|
||||||
|
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||||
|
prefix: /_error
|
7
config/routes/dev/web_profiler.yaml
Normal file
7
config/routes/dev/web_profiler.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
web_profiler_wdt:
|
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||||
|
prefix: /_wdt
|
||||||
|
|
||||||
|
web_profiler_profiler:
|
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||||
|
prefix: /_profiler
|
2
config/routes/liip_imagine.yaml
Normal file
2
config/routes/liip_imagine.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
_liip_imagine:
|
||||||
|
resource: "@LiipImagineBundle/Resources/config/routing.yaml"
|
7
config/routes/scheb_2fa.yaml
Normal file
7
config/routes/scheb_2fa.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
2fa_login:
|
||||||
|
path: /2fa
|
||||||
|
defaults:
|
||||||
|
_controller: "scheb_two_factor.form_controller:form"
|
||||||
|
|
||||||
|
2fa_login_check:
|
||||||
|
path: /2fa_check
|
65
config/services.yaml
Normal file
65
config/services.yaml
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# 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/configuration.html#application-related-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\Core\:
|
||||||
|
resource: '../core/'
|
||||||
|
exclude:
|
||||||
|
- '../core/DependencyInjection/'
|
||||||
|
- '../core/Entity/'
|
||||||
|
|
||||||
|
App\Core\EventListener\RedirectListener:
|
||||||
|
tags:
|
||||||
|
- { name: kernel.event_listener, event: kernel.exception }
|
||||||
|
|
||||||
|
App\Core\EventListener\AnalyticListener:
|
||||||
|
tags:
|
||||||
|
- { name: kernel.event_listener, event: kernel.request }
|
||||||
|
|
||||||
|
App\:
|
||||||
|
resource: '../src/'
|
||||||
|
exclude:
|
||||||
|
- '../src/DependencyInjection/'
|
||||||
|
- '../src/Entity/'
|
||||||
|
- '../src/Kernel.php'
|
||||||
|
- '../src/Tests/'
|
||||||
|
|
||||||
|
App\Core\Maker\:
|
||||||
|
resource: '../core/Maker/'
|
||||||
|
tags: ['maker.command']
|
||||||
|
|
||||||
|
App\Core\Controller\:
|
||||||
|
resource: '../core/Controller/'
|
||||||
|
tags: ['controller.service_arguments']
|
||||||
|
|
||||||
|
App\Controller\:
|
||||||
|
resource: '../src/Controller/'
|
||||||
|
tags: ['controller.service_arguments']
|
||||||
|
|
||||||
|
site.route_loader:
|
||||||
|
class: App\Core\Router\SiteRouteLoader
|
||||||
|
tags: [routing.loader]
|
||||||
|
|
||||||
|
gedmo.listener.tree:
|
||||||
|
class: Gedmo\Tree\TreeListener
|
||||||
|
tags:
|
||||||
|
- { name: doctrine.event_subscriber, connection: default }
|
||||||
|
calls:
|
||||||
|
- [ setAnnotationReader, [ "@annotation_reader" ] ]
|
||||||
|
|
||||||
|
App\UrlGenerator\FooUrlGenerator:
|
||||||
|
public: true
|
||||||
|
|
||||||
|
# add more service definitions when explicit configuration is needed
|
||||||
|
# please note that last definitions always *replace* previous ones
|
204
core/Analytic/DateRangeAnalytic.php
Normal file
204
core/Analytic/DateRangeAnalytic.php
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Analytic;
|
||||||
|
|
||||||
|
use App\Core\Entity\Site\Node;
|
||||||
|
use App\Core\Repository\Analytic\RefererRepositoryQuery;
|
||||||
|
use App\Core\Repository\Analytic\ViewRepositoryQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class DateRangeAnalytic.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class DateRangeAnalytic
|
||||||
|
{
|
||||||
|
protected ViewRepositoryQuery $viewQuery;
|
||||||
|
protected RefererRepositoryQuery $refererQuery;
|
||||||
|
protected ?Node $node;
|
||||||
|
protected ?\DateTime $from;
|
||||||
|
protected ?\DateTime $to;
|
||||||
|
protected bool $reload = true;
|
||||||
|
protected array $cache = [];
|
||||||
|
|
||||||
|
public function __construct(ViewRepositoryQuery $viewQuery, RefererRepositoryQuery $refererQuery)
|
||||||
|
{
|
||||||
|
$this->viewQuery = $viewQuery;
|
||||||
|
$this->refererQuery = $refererQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getViews(): array
|
||||||
|
{
|
||||||
|
$entities = $this->getEntities('view');
|
||||||
|
$this->reload = false;
|
||||||
|
|
||||||
|
if ($entities) {
|
||||||
|
$first = $entities[0];
|
||||||
|
$last = $entities[count($entities) - 1];
|
||||||
|
|
||||||
|
$diff = $first->getDate()->diff($last->getDate());
|
||||||
|
|
||||||
|
if ($diff->days >= 90) {
|
||||||
|
$format = 'Y-m';
|
||||||
|
} else {
|
||||||
|
$format = 'Y-m-d';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
foreach ($entities as $entity) {
|
||||||
|
$index = $entity->getDate()->format($format);
|
||||||
|
|
||||||
|
if (!isset($datas[$index])) {
|
||||||
|
$datas[$index] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas[$index] += $entity->getViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $datas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPathViews(): array
|
||||||
|
{
|
||||||
|
$entities = $this->getEntities('view');
|
||||||
|
$this->reload = false;
|
||||||
|
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
foreach ($entities as $entity) {
|
||||||
|
$index = $entity->getPath();
|
||||||
|
|
||||||
|
if (!isset($datas[$index])) {
|
||||||
|
$datas[$index] = [
|
||||||
|
'views' => 0,
|
||||||
|
'desktopViews' => 0,
|
||||||
|
'mobileViews' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas[$index]['views'] += $entity->getViews();
|
||||||
|
$datas[$index]['desktopViews'] += $entity->getDesktopViews();
|
||||||
|
$datas[$index]['mobileViews'] += $entity->getMobileViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
uasort($datas, function($a, $b) {
|
||||||
|
if ($a['views'] > $b['views']) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a['views'] < $b['views']) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $datas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReferers(): array
|
||||||
|
{
|
||||||
|
$entities = $this->getEntities('referer');
|
||||||
|
$this->reload = false;
|
||||||
|
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
foreach ($entities as $entity) {
|
||||||
|
$index = parse_url($entity->getUri(), PHP_URL_HOST);
|
||||||
|
|
||||||
|
if (!isset($datas[$index])) {
|
||||||
|
$datas[$index] = [
|
||||||
|
'views' => 0,
|
||||||
|
'uris' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas[$index]['views'] += $entity->getViews();
|
||||||
|
|
||||||
|
$path = parse_url($entity->getUri(), PHP_URL_PATH);
|
||||||
|
|
||||||
|
if (empty($path)) {
|
||||||
|
$path = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($datas[$index]['uris'][$path])) {
|
||||||
|
$datas[$index]['uris'][$path] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas[$index]['uris'][$path] += $entity->getViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
uasort($datas, function($a, $b) {
|
||||||
|
if ($a['views'] > $b['views']) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a['views'] < $b['views']) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $datas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateRange(?\DateTime $from, ?\DateTime $to): self
|
||||||
|
{
|
||||||
|
$this->from = $from;
|
||||||
|
$this->to = $to;
|
||||||
|
$this->reload = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNode(?Node $node): self
|
||||||
|
{
|
||||||
|
$this->node = $node;
|
||||||
|
$this->reload = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getEntities(string $type): array
|
||||||
|
{
|
||||||
|
if ('view' === $type) {
|
||||||
|
$query = $this->viewQuery->create();
|
||||||
|
} elseif ('referer' === $type) {
|
||||||
|
$query = $this->refererQuery->create();
|
||||||
|
} else {
|
||||||
|
throw new \InvalidArgumentException('Invalid type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->reload && isset($this->cache[$type])) {
|
||||||
|
return $this->cache[$type];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->from) {
|
||||||
|
$query
|
||||||
|
->andWhere('.date >= :from')
|
||||||
|
->setParameter(':from', $this->from)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->to) {
|
||||||
|
$query
|
||||||
|
->andWhere('.date <= :to')
|
||||||
|
->setParameter(':to', $this->to)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->node) {
|
||||||
|
$query
|
||||||
|
->andWhere('.node = :node')
|
||||||
|
->setParameter(':node', $this->node->getId())
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cache[$type] = $query->orderBy('.date')->find();
|
||||||
|
|
||||||
|
return $this->cache[$type];
|
||||||
|
}
|
||||||
|
}
|
20
core/Annotation/UrlGenerator.php
Normal file
20
core/Annotation/UrlGenerator.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Annotation;
|
||||||
|
|
||||||
|
use Doctrine\Common\Annotations\Annotation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class UrlGenerator.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
* @Annotation
|
||||||
|
*/
|
||||||
|
class UrlGenerator
|
||||||
|
{
|
||||||
|
public string $service;
|
||||||
|
|
||||||
|
public string $method;
|
||||||
|
|
||||||
|
public array $options = [];
|
||||||
|
}
|
96
core/Authenticator/LoginFormAuthenticator.php
Normal file
96
core/Authenticator/LoginFormAuthenticator.php
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Authenticator;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||||
|
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
|
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||||
|
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||||
|
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
|
||||||
|
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||||
|
|
||||||
|
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
|
||||||
|
{
|
||||||
|
use TargetPathTrait;
|
||||||
|
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
private UrlGeneratorInterface $urlGenerator;
|
||||||
|
|
||||||
|
private CsrfTokenManagerInterface $csrfTokenManager;
|
||||||
|
|
||||||
|
private UserPasswordEncoderInterface $passwordEncoder;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
|
||||||
|
{
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
|
$this->csrfTokenManager = $csrfTokenManager;
|
||||||
|
$this->passwordEncoder = $passwordEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(Request $request)
|
||||||
|
{
|
||||||
|
return 'auth_login' === $request->attributes->get('_route') && $request->isMethod('POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCredentials(Request $request)
|
||||||
|
{
|
||||||
|
$credentials = [
|
||||||
|
'email' => $request->request->get('_username'),
|
||||||
|
'password' => $request->request->get('_password'),
|
||||||
|
'csrf_token' => $request->request->get('_csrf_token'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$request->getSession()->set(Security::LAST_USERNAME, $credentials['email']);
|
||||||
|
|
||||||
|
return $credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser($credentials, UserProviderInterface $userProvider)
|
||||||
|
{
|
||||||
|
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
|
||||||
|
|
||||||
|
if (!$this->csrfTokenManager->isTokenValid($token)) {
|
||||||
|
throw new InvalidCsrfTokenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
// fail authentication with a custom error
|
||||||
|
throw new CustomUserMessageAuthenticationException('Email could not be found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkCredentials($credentials, UserInterface $user)
|
||||||
|
{
|
||||||
|
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
|
||||||
|
{
|
||||||
|
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
|
||||||
|
return new RedirectResponse($targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RedirectResponse($this->urlGenerator->generate('admin_dashboard_index'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLoginUrl()
|
||||||
|
{
|
||||||
|
return $this->urlGenerator->generate('auth_login');
|
||||||
|
}
|
||||||
|
}
|
24
core/Bundle/CoreBundle.php
Normal file
24
core/Bundle/CoreBundle.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Twig.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Core\Bundle;
|
||||||
|
|
||||||
|
use App\Core\DependencyInjection\CoreExtension;
|
||||||
|
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||||
|
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
|
||||||
|
|
||||||
|
class CoreBundle extends Bundle
|
||||||
|
{
|
||||||
|
public function getContainerExtension(): ?ExtensionInterface
|
||||||
|
{
|
||||||
|
return new CoreExtension();
|
||||||
|
}
|
||||||
|
}
|
73
core/Cache/SymfonyCacheManager.php
Normal file
73
core/Cache/SymfonyCacheManager.php
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Cache;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArrayInput;
|
||||||
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
use Symfony\Component\HttpKernel\KernelInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class SymfonyCacheManager.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class SymfonyCacheManager
|
||||||
|
{
|
||||||
|
protected KernelInterface $kernel;
|
||||||
|
protected HttpClientInterface $httpClient;
|
||||||
|
protected UrlGeneratorInterface $urlGenerator;
|
||||||
|
|
||||||
|
public function __construct(KernelInterface $kernel, HttpClientInterface $httpClient, UrlGeneratorInterface $urlGenerator)
|
||||||
|
{
|
||||||
|
$this->kernel = $kernel;
|
||||||
|
$this->httpClient = $httpClient;
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cleanRouting()
|
||||||
|
{
|
||||||
|
$finder = new Finder();
|
||||||
|
$finder
|
||||||
|
->in($this->kernel->getCacheDir())
|
||||||
|
->depth('== 0')
|
||||||
|
->name('url_*.php*')
|
||||||
|
;
|
||||||
|
|
||||||
|
$pingUrl = $this->urlGenerator->generate('_ping', [], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||||
|
|
||||||
|
foreach ($finder as $file) {
|
||||||
|
unlink((string) $file->getPathname());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hack: used to regenerate cache of url generator
|
||||||
|
$this->httpClient->request('POST', $pingUrl);
|
||||||
|
} catch (ClientException $e) {
|
||||||
|
} catch (TransportException $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cleanAll(OutputInterface $output = null)
|
||||||
|
{
|
||||||
|
$application = new Application($this->kernel);
|
||||||
|
$application->setAutoExit(false);
|
||||||
|
|
||||||
|
if (null === $output) {
|
||||||
|
$output = new BufferedOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = new ArrayInput([
|
||||||
|
'command' => 'cache:warmup',
|
||||||
|
'-e' => $this->kernel->getEnvironment(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$application->run($input, $output);
|
||||||
|
}
|
||||||
|
}
|
108
core/Command/UserCreateCommand.php
Normal file
108
core/Command/UserCreateCommand.php
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Command;
|
||||||
|
|
||||||
|
use App\Core\Factory\UserFactory;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||||
|
use Symfony\Component\Console\Question\Question;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
|
||||||
|
|
||||||
|
class UserCreateCommand extends Command
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'murph:user:create';
|
||||||
|
protected static $defaultDescription = 'Creates a user';
|
||||||
|
protected UserFactory $userFactory;
|
||||||
|
protected EntityManager $entityManager;
|
||||||
|
protected TokenGeneratorInterface $tokenGenerator;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
UserFactory $userFactory,
|
||||||
|
EntityManager $entityManager,
|
||||||
|
TokenGeneratorInterface $tokenGenerator
|
||||||
|
) {
|
||||||
|
$this->userFactory = $userFactory;
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->tokenGenerator = $tokenGenerator;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription(self::$defaultDescription)
|
||||||
|
->addArgument('email', InputArgument::OPTIONAL, 'E-mail')
|
||||||
|
->addOption('is-admin', null, InputOption::VALUE_NONE, 'Add the admin role')
|
||||||
|
->addOption('is-writer', null, InputOption::VALUE_NONE, 'Add the write role')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$helper = $this->getHelper('question');
|
||||||
|
|
||||||
|
$emailQuestion = new Question('E-mail: ');
|
||||||
|
$emailQuestion->setValidator(function ($value) {
|
||||||
|
if (empty($value)) {
|
||||||
|
throw new \RuntimeException('The email must not be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
});
|
||||||
|
|
||||||
|
$passwordQuestion = new Question('Password (leave empty to generate a random password): ');
|
||||||
|
$passwordQuestion->setHidden(true);
|
||||||
|
|
||||||
|
$isAdminDefault = $input->getOption('is-admin');
|
||||||
|
$isWriterDefault = $input->getOption('is-writer');
|
||||||
|
|
||||||
|
$isAdminQuestionLabel = sprintf('Administrator [%s] ', $isAdminDefault ? 'Y/n' : 'y/N');
|
||||||
|
$isWriterQuestionLabel = sprintf('Writer [%s] ', $isWriterDefault ? 'Y/n' : 'y/N');
|
||||||
|
|
||||||
|
$isAdminQuestion = new ConfirmationQuestion($isAdminQuestionLabel, $isAdminDefault);
|
||||||
|
$isWriterQuestion = new ConfirmationQuestion($isWriterQuestionLabel, $isWriterDefault);
|
||||||
|
|
||||||
|
$io->section('Authentication');
|
||||||
|
|
||||||
|
$email = $input->getArgument('email');
|
||||||
|
|
||||||
|
if (empty($email)) {
|
||||||
|
$email = $helper->ask($input, $output, $emailQuestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
$password = $helper->ask($input, $output, $passwordQuestion);
|
||||||
|
|
||||||
|
$showPassword = empty($password);
|
||||||
|
|
||||||
|
if ($showPassword) {
|
||||||
|
$password = mb_substr($this->tokenGenerator->generateToken(), 0, 18);
|
||||||
|
$io->info(sprintf('Password: %s', $password));
|
||||||
|
} else {
|
||||||
|
$io->newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->section('Roles');
|
||||||
|
|
||||||
|
$isAdmin = $helper->ask($input, $output, $isAdminQuestion);
|
||||||
|
$isWriter = $helper->ask($input, $output, $isWriterQuestion);
|
||||||
|
|
||||||
|
$user = $this->userFactory->create($email, $password);
|
||||||
|
$user->setIsAdmin($isAdmin);
|
||||||
|
$user->setIsWriter($isWriter);
|
||||||
|
|
||||||
|
$this->entityManager->create($user);
|
||||||
|
|
||||||
|
$io->newLine();
|
||||||
|
$io->success('User created!');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
0
core/Controller/.gitignore
vendored
Normal file
0
core/Controller/.gitignore
vendored
Normal file
150
core/Controller/Account/AccountAdminController.php
Normal file
150
core/Controller/Account/AccountAdminController.php
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Account;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
|
||||||
|
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface as TotpAuthenticatorInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||||
|
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
|
||||||
|
use ZxcvbnPhp\Zxcvbn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/account")
|
||||||
|
*/
|
||||||
|
class AccountAdminController extends AdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/", name="admin_account")
|
||||||
|
*/
|
||||||
|
public function account(Request $request, TotpAuthenticatorInterface $totpAuthenticatorService): Response
|
||||||
|
{
|
||||||
|
$account = $this->getUser();
|
||||||
|
|
||||||
|
return $this->render('@Core/account/admin/edit.html.twig', [
|
||||||
|
'account' => $account,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/2fa", name="admin_account_2fa")
|
||||||
|
*/
|
||||||
|
public function twoFactorAuthentication(
|
||||||
|
Request $request,
|
||||||
|
GoogleAuthenticatorInterface $totpAuthenticatorService,
|
||||||
|
EntityManager $entityManager
|
||||||
|
): Response {
|
||||||
|
if ($request->isMethod('GET')) {
|
||||||
|
return $this->redirectToRoute('admin_account');
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $this->getUser();
|
||||||
|
$csrfToken = $request->request->get('_csrf_token');
|
||||||
|
$enable = (bool) $request->request->get('enable');
|
||||||
|
$code = $request->request->get('code', '');
|
||||||
|
$secret = $request->request->get('secret', '');
|
||||||
|
$qrCodeContent = null;
|
||||||
|
|
||||||
|
if ($this->isCsrfTokenValid('2fa', $csrfToken)) {
|
||||||
|
if ($enable && !$account->isTotpAuthenticationEnabled()) {
|
||||||
|
if (empty($secret)) {
|
||||||
|
$secret = $totpAuthenticatorService->generateSecret();
|
||||||
|
|
||||||
|
$account->setTotpSecret($secret);
|
||||||
|
|
||||||
|
$qrCodeContent = $totpAuthenticatorService->getQRContent($account);
|
||||||
|
} else {
|
||||||
|
$account->setTotpSecret($secret);
|
||||||
|
|
||||||
|
$qrCodeContent = $totpAuthenticatorService->getQRContent($account);
|
||||||
|
|
||||||
|
if (!$totpAuthenticatorService->checkCode($account, $code)) {
|
||||||
|
$this->addFlash('error', 'The code is not valid.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('success', 'Double authentication enabled.');
|
||||||
|
|
||||||
|
$entityManager->update($account);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_account');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$enable && $account->isTotpAuthenticationEnabled()) {
|
||||||
|
$account->setTotpSecret(null);
|
||||||
|
|
||||||
|
$entityManager->update($account);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Double authentication disabled.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_account');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/account/admin/edit.html.twig', [
|
||||||
|
'account' => $account,
|
||||||
|
'twoFaKey' => $secret,
|
||||||
|
'twoFaQrCodeContent' => $qrCodeContent,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/password", name="admin_account_password", methods={"POST"})
|
||||||
|
*/
|
||||||
|
public function password(
|
||||||
|
Request $request,
|
||||||
|
UserRepository $repository,
|
||||||
|
TokenGeneratorInterface $tokenGenerator,
|
||||||
|
UserPasswordEncoderInterface $encoder,
|
||||||
|
EntityManager $entityManager
|
||||||
|
): Response {
|
||||||
|
$account = $this->getUser();
|
||||||
|
$csrfToken = $request->request->get('_csrf_token');
|
||||||
|
|
||||||
|
if ($this->isCsrfTokenValid('password', $csrfToken)) {
|
||||||
|
$password = $request->request->get('password');
|
||||||
|
|
||||||
|
if (!$encoder->isPasswordValid($account, $password)) {
|
||||||
|
$this->addFlash('error', 'The form is not valid.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_account');
|
||||||
|
}
|
||||||
|
|
||||||
|
$password1 = $request->request->get('password1');
|
||||||
|
$password2 = $request->request->get('password2');
|
||||||
|
|
||||||
|
$zxcvbn = new Zxcvbn();
|
||||||
|
$strength = $zxcvbn->passwordStrength($password1, []);
|
||||||
|
|
||||||
|
if (4 === $strength['score'] && $password1 === $password2) {
|
||||||
|
$account
|
||||||
|
->setPassword($encoder->encodePassword($account, $password1))
|
||||||
|
->setConfirmationToken($tokenGenerator->generateToken())
|
||||||
|
;
|
||||||
|
|
||||||
|
$entityManager->update($account);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Password updated.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_account');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFlash('error', 'The form is not valid.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_account');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function getSection(): string
|
||||||
|
{
|
||||||
|
return 'account';
|
||||||
|
}
|
||||||
|
}
|
40
core/Controller/Admin/AdminController.php
Normal file
40
core/Controller/Admin/AdminController.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Admin;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
abstract class AdminController extends AbstractController
|
||||||
|
{
|
||||||
|
protected array $coreParameters;
|
||||||
|
|
||||||
|
public function __construct(ParameterBagInterface $parameters)
|
||||||
|
{
|
||||||
|
$this->coreParameters = $parameters->get('core');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/_ping", name="_ping")
|
||||||
|
*/
|
||||||
|
public function ping()
|
||||||
|
{
|
||||||
|
return $this->json(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function render(string $view, array $parameters = [], Response $response = null): Response
|
||||||
|
{
|
||||||
|
$parameters['section'] = $this->getSection();
|
||||||
|
$parameters['site_name'] = $this->coreParameters['site']['name'];
|
||||||
|
$parameters['site_logo'] = $this->coreParameters['site']['logo'];
|
||||||
|
|
||||||
|
return parent::render($view, $parameters, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function getSection(): string;
|
||||||
|
}
|
342
core/Controller/Admin/Crud/CrudController.php
Normal file
342
core/Controller/Admin/Crud/CrudController.php
Normal file
|
@ -0,0 +1,342 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Admin\Crud;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use App\Core\Crud\CrudConfiguration;
|
||||||
|
use App\Core\Entity\EntityInterface;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use App\Core\Repository\RepositoryQuery;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class CrudController.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
abstract class CrudController extends AdminController
|
||||||
|
{
|
||||||
|
protected array $filters = [];
|
||||||
|
protected array $sort = [
|
||||||
|
'label' => null,
|
||||||
|
'direction' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
abstract protected function getConfiguration(): CrudConfiguration;
|
||||||
|
|
||||||
|
protected function doIndex(int $page = 1, RepositoryQuery $query, Request $request, Session $session): Response
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
|
$this->applySort('index', $query, $request);
|
||||||
|
$this->updateFilters($request, $session);
|
||||||
|
|
||||||
|
$pager = $query
|
||||||
|
->usefilters($this->filters)
|
||||||
|
->paginate($page, $configuration->getmaxperpage('index'))
|
||||||
|
;
|
||||||
|
|
||||||
|
return $this->render($this->getConfiguration()->getView('index'), [
|
||||||
|
'configuration' => $configuration,
|
||||||
|
'pager' => $pager,
|
||||||
|
'sort' => $this->sort,
|
||||||
|
'filters' => [
|
||||||
|
'show' => null !== $configuration->getForm('filter'),
|
||||||
|
'isEmpty' => empty($this->filters),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doNew(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeCreate = null): Response
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
|
$this->prepareEntity($entity);
|
||||||
|
|
||||||
|
$form = $this->createForm($configuration->getForm('new'), $entity, $configuration->getFormOptions('new'));
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
if (null !== $beforeCreate) {
|
||||||
|
call_user_func_array($beforeCreate, [$entity, $form, $request]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entityManager->create($entity);
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute($configuration->getPageRoute('edit'), array_merge(
|
||||||
|
['entity' => $entity->getId()],
|
||||||
|
$configuration->getPageRouteParams('edit')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render($configuration->getView('new'), [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'configuration' => $configuration,
|
||||||
|
'entity' => $entity,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doShow(EntityInterface $entity): Response
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
|
return $this->render($configuration->getView('show'), [
|
||||||
|
'entity' => $entity,
|
||||||
|
'configuration' => $configuration,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doEdit(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeUpdate = null): Response
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
|
$this->prepareEntity($entity);
|
||||||
|
|
||||||
|
$form = $this->createForm($configuration->getForm('edit'), $entity, $configuration->getFormOptions('edit'));
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
if (null !== $beforeUpdate) {
|
||||||
|
call_user_func_array($beforeUpdate, [$entity, $form, $request]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entityManager->update($entity);
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute($configuration->getPageRoute('edit'), array_merge(
|
||||||
|
['entity' => $entity->getId()],
|
||||||
|
$configuration->getPageRouteParams('edit')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render($configuration->getView('edit'), [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'configuration' => $configuration,
|
||||||
|
'entity' => $entity,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSort(int $page = 1, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
$context = $request->query->get('context', 'index');
|
||||||
|
|
||||||
|
if (!$configuration->getIsSortableCollection($context)) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applySort($context, $query, $request);
|
||||||
|
$this->updateFilters($request, $session);
|
||||||
|
|
||||||
|
$pager = $query
|
||||||
|
->useFilters($this->filters)
|
||||||
|
->paginate($page, $configuration->getMaxPerPage($context))
|
||||||
|
;
|
||||||
|
|
||||||
|
if ($this->isCsrfTokenValid('sort', $request->query->get('_token'))) {
|
||||||
|
$items = $request->request->get('items', []);
|
||||||
|
$setter = 'set'.$configuration->getSortableCollectionProperty();
|
||||||
|
$orderStart = ($page - 1) * $configuration->getMaxPerPage($context);
|
||||||
|
|
||||||
|
foreach ($pager as $key => $entity) {
|
||||||
|
if (isset($items[$key + 1])) {
|
||||||
|
$entity->{$setter}($items[$key + 1] + $orderStart);
|
||||||
|
|
||||||
|
$entityManager->update($entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doBatch(int $page = 1, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
$datas = $request->request->get('batch', []);
|
||||||
|
|
||||||
|
$context = $datas['context'] ?? 'index';
|
||||||
|
$target = $datas['target'] ?? null;
|
||||||
|
$action = $datas['action'] ?? null;
|
||||||
|
$token = $datas['_token'] ?? null;
|
||||||
|
$items = $datas['items'] ?? [];
|
||||||
|
$batchAction = $configuration->getBatchAction($context, $action);
|
||||||
|
|
||||||
|
if (empty($context) || empty($action) || empty($target)) {
|
||||||
|
return $this->json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isCsrfTokenValid('batch', $token) || empty($batchAction)) {
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
|
||||||
|
return $this->json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$callback = $batchAction['callback'];
|
||||||
|
|
||||||
|
$this->applySort($context, $query, $request);
|
||||||
|
$this->updateFilters($request, $session);
|
||||||
|
|
||||||
|
$query->useFilters($this->filters);
|
||||||
|
|
||||||
|
if ('selection' === $target) {
|
||||||
|
$isSelection = true;
|
||||||
|
$pager = $query->paginate($page, $configuration->getMaxPerPage($context));
|
||||||
|
} else {
|
||||||
|
$isSelection = false;
|
||||||
|
$pager = $query->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pager as $key => $entity) {
|
||||||
|
if (($isSelection && isset($items[$key + 1])) || !$isSelection) {
|
||||||
|
$callback($entity, $entityManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Batch action done.');
|
||||||
|
|
||||||
|
return $this->json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doDelete(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeDelete = null): Response
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
|
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
|
||||||
|
if (null !== $beforeDelete) {
|
||||||
|
call_user_func($beforeDelete, $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entityManager->delete($entity);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been removed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute($configuration->getPageRoute('index'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doFilter(Session $session): Response
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
$type = $configuration->getForm('filter');
|
||||||
|
|
||||||
|
if (null === $type) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm($type);
|
||||||
|
$form->submit($session->get($form->getName(), []));
|
||||||
|
|
||||||
|
return $this->render($configuration->getView('filter'), [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'configuration' => $configuration,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function updateFilters(Request $request, Session $session)
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
$type = $configuration->getForm('filter');
|
||||||
|
|
||||||
|
if (null === $type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm($type);
|
||||||
|
|
||||||
|
if ($request->query->has($form->getName())) {
|
||||||
|
$filters = $request->query->get($form->getName());
|
||||||
|
|
||||||
|
if ('0' === $filters) {
|
||||||
|
$filters = [];
|
||||||
|
}
|
||||||
|
} elseif ($session->has($form->getName())) {
|
||||||
|
$filters = $session->get($form->getName());
|
||||||
|
} else {
|
||||||
|
$filters = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$form->submit($filters);
|
||||||
|
|
||||||
|
if (empty($filters)) {
|
||||||
|
$this->filters = $filters;
|
||||||
|
$session->set($form->getName(), $filters);
|
||||||
|
} elseif ($form->isValid()) {
|
||||||
|
$this->filters = $form->getData();
|
||||||
|
$session->set($form->getName(), $filters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareEntity(EntityInterface $entity)
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
|
if ($configuration->isI18n()) {
|
||||||
|
foreach ($configuration->getLocales() as $locale) {
|
||||||
|
$entity->addTranslation($entity->translate($locale, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function applySort(string $context, RepositoryQuery $query, Request $request)
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
|
if ($configuration->getIsSortableCollection($context)) {
|
||||||
|
$query->orderBy(sprintf('.%s', $configuration->getSortableCollectionProperty()));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultSort = $configuration->getDefaultSort($context);
|
||||||
|
|
||||||
|
$name = $request->query->get('_sort', $defaultSort['label'] ?? null);
|
||||||
|
$direction = strtolower($request->query->get('_sort_direction', $defaultSort['direction'] ?? 'asc'));
|
||||||
|
|
||||||
|
if (!in_array($direction, ['asc', 'desc'])) {
|
||||||
|
$direction = 'asc';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($configuration->getFields($context) as $label => $field) {
|
||||||
|
$sortOption = $field['options']['sort'] ?? null;
|
||||||
|
|
||||||
|
if (null === $sortOption) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sortOption[0] !== $name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sorter = $sortOption[1];
|
||||||
|
|
||||||
|
if (is_string($sorter)) {
|
||||||
|
$query->orderBy($sorter, $direction);
|
||||||
|
} else {
|
||||||
|
call_user_func_array($sorter, [$query, $direction]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sort = [
|
||||||
|
'label' => $label,
|
||||||
|
'direction' => $direction,
|
||||||
|
];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
core/Controller/Analytic/AnalyticController.php
Normal file
38
core/Controller/Analytic/AnalyticController.php
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Analytic;
|
||||||
|
|
||||||
|
use App\Core\Analytic\DateRangeAnalytic;
|
||||||
|
use App\Core\Entity\Site\Node;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/analytic")
|
||||||
|
*/
|
||||||
|
class AnalyticController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/stats/{node}/{range}", name="admin_analytic_stats")
|
||||||
|
*/
|
||||||
|
public function stats(Node $node, DateRangeAnalytic $analytic, string $range = '7days'): Response
|
||||||
|
{
|
||||||
|
if (!in_array($range, ['7days', '30days', '90days', '1year'])) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$analytic
|
||||||
|
->setDateRange(new \DateTime('now - '.$range), new \DateTime())
|
||||||
|
->setNode($node)
|
||||||
|
;
|
||||||
|
|
||||||
|
return $this->render('@Core/analytic/stats.html.twig', [
|
||||||
|
'range' => $range,
|
||||||
|
'views' => $analytic->getViews(),
|
||||||
|
'pathViews' => $analytic->getPathViews(),
|
||||||
|
'referers' => $analytic->getReferers(),
|
||||||
|
'node' => $node,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
155
core/Controller/Auth/AuthController.php
Normal file
155
core/Controller/Auth/AuthController.php
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Auth;
|
||||||
|
|
||||||
|
use App\Core\Event\Account\PasswordRequestEvent;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||||
|
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
use ZxcvbnPhp\Zxcvbn;
|
||||||
|
|
||||||
|
class AuthController extends AbstractController
|
||||||
|
{
|
||||||
|
protected array $coreParameters;
|
||||||
|
|
||||||
|
public function __construct(ParameterBagInterface $parameters)
|
||||||
|
{
|
||||||
|
$this->coreParameters = $parameters->get('core');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/login", name="auth_login")
|
||||||
|
*/
|
||||||
|
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||||
|
{
|
||||||
|
if ($this->getUser()) {
|
||||||
|
return $this->redirectToRoute('admin_dashboard_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = $authenticationUtils->getLastAuthenticationError();
|
||||||
|
$lastUsername = $authenticationUtils->getLastUsername();
|
||||||
|
|
||||||
|
return $this->render('@Core/auth/login.html.twig', [
|
||||||
|
'last_username' => $lastUsername,
|
||||||
|
'error' => $error,
|
||||||
|
'site_name' => $this->coreParameters['site']['name'],
|
||||||
|
'site_logo' => $this->coreParameters['site']['logo'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/resetting/request", name="auth_resetting_request")
|
||||||
|
*/
|
||||||
|
public function requestResetting(Request $request, UserRepository $repository, EventDispatcherInterface $eventDispatcher): Response
|
||||||
|
{
|
||||||
|
if ($this->getUser()) {
|
||||||
|
return $this->redirectToRoute('admin_dashboard_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$csrfToken = $request->request->get('_csrf_token');
|
||||||
|
|
||||||
|
if (!$this->isCsrfTokenValid('resetting_request', $csrfToken)) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim((string) $request->request->get('username'));
|
||||||
|
|
||||||
|
if (!$username) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $repository->findOneByEmail($username);
|
||||||
|
|
||||||
|
if ($account) {
|
||||||
|
$requestedAt = $account->getPasswordRequestedAt();
|
||||||
|
|
||||||
|
if (null === $requestedAt || $requestedAt->getTimestamp() < (time() - 3600 / 2)) {
|
||||||
|
$eventDispatcher->dispatch(new PasswordRequestEvent($account), PasswordRequestEvent::EVENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/auth/resetting_request.html.twig', [
|
||||||
|
'email_sent' => $request->isMethod('POST'),
|
||||||
|
'site_name' => $this->coreParameters['site']['name'],
|
||||||
|
'site_logo' => $this->coreParameters['site']['logo'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/resetting/update/{token}", name="auth_resetting_update")
|
||||||
|
*/
|
||||||
|
public function requestUpdate(
|
||||||
|
string $token,
|
||||||
|
Request $request,
|
||||||
|
UserRepository $repository,
|
||||||
|
TokenGeneratorInterface $tokenGenerator,
|
||||||
|
UserPasswordEncoderInterface $encoder,
|
||||||
|
EntityManager $entityManager
|
||||||
|
): Response {
|
||||||
|
if ($this->getUser()) {
|
||||||
|
return $this->redirectToRoute('admin_dashboard_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $repository->findOneByConfirmationToken($token);
|
||||||
|
$passwordUpdated = false;
|
||||||
|
$expired = true;
|
||||||
|
|
||||||
|
if ($account) {
|
||||||
|
$requestedAt = $account->getPasswordRequestedAt();
|
||||||
|
$expired = (null === $requestedAt || ($requestedAt->getTimestamp() < (time() - 3600 * 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isMethod('POST') && !$expired) {
|
||||||
|
$csrfToken = $request->request->get('_csrf_token');
|
||||||
|
|
||||||
|
if ($this->isCsrfTokenValid('resetting_update', $csrfToken)) {
|
||||||
|
$password = $request->request->get('password');
|
||||||
|
$password2 = $request->request->get('password2');
|
||||||
|
|
||||||
|
$zxcvbn = new Zxcvbn();
|
||||||
|
$strength = $zxcvbn->passwordStrength($password, []);
|
||||||
|
|
||||||
|
if (4 === $strength['score'] && $password === $password2) {
|
||||||
|
$account
|
||||||
|
->setPassword($encoder->encodePassword(
|
||||||
|
$account,
|
||||||
|
$password
|
||||||
|
))
|
||||||
|
->setConfirmationToken($tokenGenerator->generateToken())
|
||||||
|
->setPasswordRequestedAt(new \DateTime('now'))
|
||||||
|
;
|
||||||
|
|
||||||
|
$entityManager->update($account);
|
||||||
|
|
||||||
|
$passwordUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/auth/resetting_update.html.twig', [
|
||||||
|
'password_updated' => $passwordUpdated,
|
||||||
|
'token' => $token,
|
||||||
|
'expired' => $expired,
|
||||||
|
'site_name' => $this->coreParameters['site']['name'],
|
||||||
|
'site_logo' => $this->coreParameters['site']['logo'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/logout", name="auth_logout")
|
||||||
|
*/
|
||||||
|
public function logout()
|
||||||
|
{
|
||||||
|
throw new \Exception('This method can be blank - it will be intercepted by the logout key on your firewall');
|
||||||
|
}
|
||||||
|
}
|
27
core/Controller/Dashboard/DashboardAdminController.php
Normal file
27
core/Controller/Dashboard/DashboardAdminController.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Dashboard;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin")
|
||||||
|
*/
|
||||||
|
class DashboardAdminController extends AdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/", name="admin_dashboard_index")
|
||||||
|
*/
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return $this->render('@Core/dashboard/index.html.twig', [
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSection(): string
|
||||||
|
{
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
}
|
438
core/Controller/FileManager/FileManagerAdminController.php
Normal file
438
core/Controller/FileManager/FileManagerAdminController.php
Normal file
|
@ -0,0 +1,438 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\FileManager;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use App\Core\FileManager\FsFileManager;
|
||||||
|
use App\Core\Form\FileManager\DirectoryCreateType;
|
||||||
|
use App\Core\Form\FileManager\DirectoryRenameType;
|
||||||
|
use App\Core\Form\FileManager\FileInformationType;
|
||||||
|
use App\Core\Form\FileManager\FileRenameType;
|
||||||
|
use App\Core\Form\FileManager\FileUploadType;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/file_manager")
|
||||||
|
*/
|
||||||
|
class FileManagerAdminController extends AdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/", name="admin_file_manager_index")
|
||||||
|
*/
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return $this->render('@Core/file_manager/index.html.twig');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/api/directory", name="admin_file_manager_api_directory", options={"expose"=true})
|
||||||
|
*/
|
||||||
|
public function directory(FsFileManager $manager, Request $request): Response
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'sort' => $request->query->get('_sort', 'name'),
|
||||||
|
'sort_direction' => $request->query->get('_sort_direction', 'asc'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$files = $manager->list($request->query->get('directory', '/'), $options);
|
||||||
|
|
||||||
|
return $this->json($files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/info/{tab}/{context}/{ajax}", name="admin_file_manager_info", options={"expose"=true})
|
||||||
|
*/
|
||||||
|
public function info(
|
||||||
|
FsFileManager $manager,
|
||||||
|
Request $request,
|
||||||
|
EntityManager $entityManager,
|
||||||
|
TranslatorInterface $translator,
|
||||||
|
string $context = 'crud',
|
||||||
|
string $tab = 'information',
|
||||||
|
bool $ajax = false
|
||||||
|
): Response {
|
||||||
|
$splInfo = $manager->getSplInfo($request->query->get('file'));
|
||||||
|
|
||||||
|
if (!$splInfo) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileInfo = $manager->getFileInformation($request->query->get('file'));
|
||||||
|
$path = $manager->getPathUri().'/'.$splInfo->getRelativePathname();
|
||||||
|
|
||||||
|
$form = $this->createForm(FileInformationType::class, $fileInfo);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$entityManager->update($fileInfo);
|
||||||
|
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 0,
|
||||||
|
'_message' => $translator->trans('The data has been saved.'),
|
||||||
|
'_level' => 'success',
|
||||||
|
'_dispatch' => 'file_manager.info.update.success',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 1,
|
||||||
|
'_message' => $translator->trans('The form is not valid.'),
|
||||||
|
'_level' => 'warning',
|
||||||
|
'_dispatch' => 'file_manager.info.update.error',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_file_manager_index', [
|
||||||
|
'data-modal' => $this->generateUrl('admin_file_manager_info', [
|
||||||
|
'file' => $request->query->get('file'),
|
||||||
|
'tab' => 'attributes',
|
||||||
|
]),
|
||||||
|
'path' => $splInfo->getRelativePath(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/file_manager/info.html.twig', [
|
||||||
|
'splInfo' => $splInfo,
|
||||||
|
'path' => $path,
|
||||||
|
'isLocked' => $manager->isLocked($splInfo->getRelativePathname()),
|
||||||
|
'tab' => $tab,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'context' => $context,
|
||||||
|
'ajax' => $ajax,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/directory/new/{ajax}", name="admin_file_manager_directory_new", options={"expose"=true}, methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function directoryNew(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
|
||||||
|
{
|
||||||
|
$splInfo = $manager->getSplInfo($request->query->get('file'));
|
||||||
|
|
||||||
|
if (!$splInfo) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$splInfo->isDir()) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($manager->isLocked($request->query->get('file'))) {
|
||||||
|
return $this->render('@Core/file_manager/directory_new.html.twig', [
|
||||||
|
'locked' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(DirectoryCreateType::class);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$status = $manager->createDirectory($form->get('name')->getData(), $request->query->get('file'));
|
||||||
|
|
||||||
|
if (true === $status) {
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('success', 'Directory created.');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 0,
|
||||||
|
'_message' => $translator->trans('Directory created.'),
|
||||||
|
'_level' => 'success',
|
||||||
|
'_dispatch' => 'file_manager.directory.new.success',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('warning', 'Directory not created.');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 1,
|
||||||
|
'_message' => $translator->trans('Directory not created.'),
|
||||||
|
'_level' => 'warning',
|
||||||
|
'_dispatch' => 'file_manager.directory.new.error',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->addFlash('warning', 'Unauthorized char(s).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_file_manager_index', [
|
||||||
|
'path' => $splInfo->getRelativePathname(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/file_manager/directory_new.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'file' => $request->query->get('file'),
|
||||||
|
'ajax' => $ajax,
|
||||||
|
'locked' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/directory/rename/{ajax}", name="admin_file_manager_directory_rename", methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function directoryRename(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
|
||||||
|
{
|
||||||
|
$splInfo = $manager->getSplInfo($request->query->get('file'));
|
||||||
|
|
||||||
|
if (!$splInfo) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$splInfo->isDir()) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($manager->isLocked($request->query->get('file'))) {
|
||||||
|
return $this->render('@Core/file_manager/directory_rename.html.twig', [
|
||||||
|
'locked' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(DirectoryRenameType::class, [
|
||||||
|
'name' => $splInfo->getFilename(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$status = $manager->renameDirectory($form->get('name')->getData(), $request->query->get('file'));
|
||||||
|
|
||||||
|
if (true === $status) {
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('success', 'Directory renamed.');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 0,
|
||||||
|
'_message' => $translator->trans('Directory renamed.'),
|
||||||
|
'_level' => 'success',
|
||||||
|
'_dispatch' => 'file_manager.directory.rename.success',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('warning', 'Directory not renamed.');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 1,
|
||||||
|
'_message' => $translator->trans('Directory not renamed.'),
|
||||||
|
'_level' => 'warning',
|
||||||
|
'_dispatch' => 'file_manager.directory.rename.error',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->addFlash('warning', 'Unauthorized char(s).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_file_manager_index', [
|
||||||
|
'path' => $splInfo->getRelativePath(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/file_manager/directory_rename.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'file' => $request->query->get('file'),
|
||||||
|
'locked' => false,
|
||||||
|
'ajax' => $ajax,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/file/rename/{ajax}", name="admin_file_manager_file_rename", methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function fileRename(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
|
||||||
|
{
|
||||||
|
$splInfo = $manager->getSplInfo($request->query->get('file'));
|
||||||
|
|
||||||
|
if (!$splInfo) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($splInfo->isDir()) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($manager->isLocked($request->query->get('file'))) {
|
||||||
|
return $this->render('@Core/file_manager/file_rename.html.twig', [
|
||||||
|
'locked' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(FileRenameType::class, [
|
||||||
|
'name' => preg_replace(sprintf('/\.%s/', $splInfo->getExtension()), '', $splInfo->getFilename()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$status = $manager->renameFile($form->get('name')->getData(), $request->query->get('file'));
|
||||||
|
|
||||||
|
if (true === $status) {
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('success', 'File renamed.');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 0,
|
||||||
|
'_message' => $translator->trans('File renamed.'),
|
||||||
|
'_level' => 'success',
|
||||||
|
'_dispatch' => 'file_manager.file.rename.success',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('warning', 'File not renamed.');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 1,
|
||||||
|
'_message' => $translator->trans('File not renamed.'),
|
||||||
|
'_level' => 'warning',
|
||||||
|
'_dispatch' => 'file_manager.file.rename.error',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->addFlash('warning', 'Unauthorized char(s).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_file_manager_index', [
|
||||||
|
'path' => $splInfo->getRelativePath(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/file_manager/file_rename.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'file' => $request->query->get('file'),
|
||||||
|
'exention' => $splInfo->getExtension(),
|
||||||
|
'locked' => false,
|
||||||
|
'ajax' => $ajax,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/upload/{ajax}", name="admin_file_manager_upload", options={"expose"=true}, methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function upload(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
|
||||||
|
{
|
||||||
|
$splInfo = $manager->getSplInfo($request->query->get('file'));
|
||||||
|
|
||||||
|
if (!$splInfo) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$splInfo->isDir()) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($manager->isLocked($request->query->get('file'))) {
|
||||||
|
return $this->render('@Core/file_manager/upload.html.twig', [
|
||||||
|
'locked' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$form = $this->createForm(FileUploadType::class, null, [
|
||||||
|
'mimes' => $manager->getMimes(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
if ($form->get('files')->getData()) {
|
||||||
|
$manager->upload(
|
||||||
|
$form->get('files')->getData(),
|
||||||
|
$request->query->get('file')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form->get('directory')->getData()) {
|
||||||
|
$manager->upload(
|
||||||
|
$form->get('directory')->getData(),
|
||||||
|
$request->query->get('file'),
|
||||||
|
$_FILES['file_upload']['full_path']['directory'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('success', 'Files uploaded.');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 0,
|
||||||
|
'_message' => $translator->trans('Files uploaded.'),
|
||||||
|
'_level' => 'success',
|
||||||
|
'_dispatch' => 'file_manager.file.new.success',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
$this->addFlash('warning', 'Unauthorized file type(s).');
|
||||||
|
} else {
|
||||||
|
return $this->json([
|
||||||
|
'_error' => 1,
|
||||||
|
'_message' => $translator->trans('Unauthorized file type(s).'),
|
||||||
|
'_level' => 'warning',
|
||||||
|
'_dispatch' => 'file_manager.file.new.error',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_file_manager_index', [
|
||||||
|
'path' => $splInfo->getRelativePathname(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/file_manager/upload.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'file' => $request->query->get('file'),
|
||||||
|
'locked' => false,
|
||||||
|
'ajax' => $ajax,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/delete", name="admin_file_manager_delete", methods={"DELETE"})
|
||||||
|
*/
|
||||||
|
public function delete(FsFileManager $manager, Request $request): Response
|
||||||
|
{
|
||||||
|
$path = $request->request->get('file');
|
||||||
|
$splInfo = $manager->getSplInfo($request->request->get('file'));
|
||||||
|
|
||||||
|
if (!$splInfo) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isCsrfTokenValid('delete', $request->request->get('_token'))) {
|
||||||
|
if ($manager->delete($path)) {
|
||||||
|
$this->addFlash('success', 'The data has been removed.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('warning', 'The data has not been removed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_file_manager_index', [
|
||||||
|
'path' => $splInfo->getRelativePath(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSection(): string
|
||||||
|
{
|
||||||
|
return 'file_manager';
|
||||||
|
}
|
||||||
|
}
|
154
core/Controller/Redirect/RedirectAdminController.php
Normal file
154
core/Controller/Redirect/RedirectAdminController.php
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Redirect;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\Crud\CrudController;
|
||||||
|
use App\Core\Crud\CrudConfiguration;
|
||||||
|
use App\Core\Crud\Field;
|
||||||
|
use App\Core\Entity\EntityInterface;
|
||||||
|
use App\Core\Entity\Redirect as Entity;
|
||||||
|
use App\Core\Factory\RedirectFactory as Factory;
|
||||||
|
use App\Core\Form\Filter\RedirectFilterType as FilterType;
|
||||||
|
use App\Core\Form\RedirectType as Type;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use App\Core\Repository\RedirectRepositoryQuery as RepositoryQuery;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
class RedirectAdminController extends CrudController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/admin/redirect/{page}", name="admin_redirect_index", methods={"GET"}, requirements={"page":"\d+"})
|
||||||
|
*/
|
||||||
|
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
|
||||||
|
{
|
||||||
|
return $this->doIndex($page, $query, $request, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/redirect/new", name="admin_redirect_new", methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doNew($factory->create(), $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/redirect/show/{entity}", name="admin_redirect_show", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function show(Entity $entity): Response
|
||||||
|
{
|
||||||
|
return $this->doShow($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/redirect/filter", name="admin_redirect_filter", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function filter(Session $session): Response
|
||||||
|
{
|
||||||
|
return $this->doFilter($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/redirect/edit/{entity}", name="admin_redirect_edit", methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doEdit($entity, $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/redirect/sort/{page}", name="admin_redirect_sort", methods={"POST"}, requirements={"page":"\d+"})
|
||||||
|
*/
|
||||||
|
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
|
||||||
|
{
|
||||||
|
return $this->doSort($page, $query, $entityManager, $request, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/redirect/batch/{page}", name="admin_redirect_batch", methods={"POST"}, requirements={"page":"\d+"})
|
||||||
|
*/
|
||||||
|
public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
|
||||||
|
{
|
||||||
|
return $this->doBatch($page, $query, $entityManager, $request, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/redirect/delete/{entity}", name="admin_redirect_delete", methods={"DELETE"})
|
||||||
|
*/
|
||||||
|
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doDelete($entity, $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getConfiguration(): CrudConfiguration
|
||||||
|
{
|
||||||
|
return CrudConfiguration::create()
|
||||||
|
->setPageTitle('index', 'Redirects')
|
||||||
|
->setPageTitle('edit', '{label}')
|
||||||
|
->setPageTitle('new', 'New redirect')
|
||||||
|
|
||||||
|
->setPageRoute('index', 'admin_redirect_index')
|
||||||
|
->setPageRoute('new', 'admin_redirect_new')
|
||||||
|
->setPageRoute('edit', 'admin_redirect_edit')
|
||||||
|
->setPageRoute('sort', 'admin_redirect_sort')
|
||||||
|
->setPageRoute('batch', 'admin_redirect_batch')
|
||||||
|
->setPageRoute('delete', 'admin_redirect_delete')
|
||||||
|
->setPageRoute('filter', 'admin_redirect_filter')
|
||||||
|
|
||||||
|
->setForm('edit', Type::class, [])
|
||||||
|
->setForm('new', Type::class)
|
||||||
|
->setForm('filter', FilterType::class)
|
||||||
|
|
||||||
|
->setView('form', '@Core/redirect/redirect_admin/_form.html.twig')
|
||||||
|
->setMaxPerPage('index', 100)
|
||||||
|
->setIsSortableCollection('index', true)
|
||||||
|
|
||||||
|
->setAction('index', 'show', false)
|
||||||
|
->setAction('edit', 'show', false)
|
||||||
|
|
||||||
|
->setField('index', 'Label', Field\TextField::class, [
|
||||||
|
'property' => 'label',
|
||||||
|
'attr' => ['class' => 'col-4'],
|
||||||
|
])
|
||||||
|
->setField('index', 'Rule', Field\TextField::class, [
|
||||||
|
'view' => '@Core/redirect/redirect_admin/field/rule.html.twig',
|
||||||
|
'attr' => ['class' => 'col-6'],
|
||||||
|
])
|
||||||
|
->setField('index', 'Enabled', Field\ButtonField::class, [
|
||||||
|
'property_builder' => function(EntityInterface $entity) {
|
||||||
|
return $entity->getIsEnabled() ? 'Yes' : 'No';
|
||||||
|
},
|
||||||
|
'attr' => ['class' => 'col-1'],
|
||||||
|
'button_attr_builder' => function(EntityInterface $entity) {
|
||||||
|
return ['class' => 'btn btn-sm btn-'.($entity->getIsEnabled() ? 'success' : 'primary')];
|
||||||
|
},
|
||||||
|
])
|
||||||
|
->setField('index', 'Type', Field\ButtonField::class, [
|
||||||
|
'property' => 'redirectCode',
|
||||||
|
'attr' => ['class' => 'col-1'],
|
||||||
|
'button_attr' => ['class' => 'btn btn-sm btn-light border-secondary font-weight-bold'],
|
||||||
|
])
|
||||||
|
->setBatchAction('index', 'enable', 'Enable', function (EntityInterface $entity, EntityManager $manager) {
|
||||||
|
$entity->setIsEnabled(true);
|
||||||
|
|
||||||
|
$manager->update($entity);
|
||||||
|
})
|
||||||
|
->setBatchAction('index', 'disable', 'Disable', function (EntityInterface $entity, EntityManager $manager) {
|
||||||
|
$entity->setIsEnabled(false);
|
||||||
|
|
||||||
|
$manager->update($entity);
|
||||||
|
})
|
||||||
|
->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) {
|
||||||
|
$manager->delete($entity);
|
||||||
|
})
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSection(): string
|
||||||
|
{
|
||||||
|
return 'site_navigation';
|
||||||
|
}
|
||||||
|
}
|
81
core/Controller/Setting/NavigationSettingAdminController.php
Normal file
81
core/Controller/Setting/NavigationSettingAdminController.php
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Setting;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use App\Core\Entity\NavigationSetting as Entity;
|
||||||
|
use App\Core\Event\Setting\NavigationSettingEvent;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/navigation_setting")
|
||||||
|
*/
|
||||||
|
class NavigationSettingAdminController extends AdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/edit/{entity}", name="admin_navigation_setting_edit")
|
||||||
|
*/
|
||||||
|
public function edit(
|
||||||
|
Entity $entity,
|
||||||
|
EntityManager $entityManager,
|
||||||
|
EventDispatcherInterface $eventDispatcher,
|
||||||
|
Request $request
|
||||||
|
): Response {
|
||||||
|
$builder = $this->createFormBuilder($entity);
|
||||||
|
$event = new NavigationSettingEvent([
|
||||||
|
'builder' => $builder,
|
||||||
|
'entity' => $entity,
|
||||||
|
'options' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$eventDispatcher->dispatch($event, NavigationSettingEvent::FORM_INIT_EVENT);
|
||||||
|
|
||||||
|
$form = $builder->getForm();
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$entityManager->update($entity);
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_site_navigation_show', [
|
||||||
|
'entity' => $entity->getNavigation()->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/setting/navigation_setting_admin/edit.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'entity' => $entity,
|
||||||
|
'options' => $event->getData()['options'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/delete/{entity}", name="admin_navigation_setting_delete", methods={"DELETE"})
|
||||||
|
*/
|
||||||
|
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
|
||||||
|
$entityManager->delete($entity);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been removed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_site_navigation_show', [
|
||||||
|
'entity' => $entity->getNavigation()->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSection(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
99
core/Controller/Setting/SettingAdminController.php
Normal file
99
core/Controller/Setting/SettingAdminController.php
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Setting;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use App\Core\Entity\Setting as Entity;
|
||||||
|
use App\Core\Event\Setting\SettingEvent;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use App\Core\Repository\SettingRepositoryQuery as RepositoryQuery;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/setting")
|
||||||
|
*/
|
||||||
|
class SettingAdminController extends AdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/{page}", name="admin_setting_index", requirements={"page": "\d+"})
|
||||||
|
*/
|
||||||
|
public function index(
|
||||||
|
RepositoryQuery $query,
|
||||||
|
EventDispatcherInterface $eventDispatcher,
|
||||||
|
Request $request,
|
||||||
|
int $page = 1
|
||||||
|
): Response {
|
||||||
|
$eventDispatcher->dispatch(new SettingEvent(), SettingEvent::INIT_EVENT);
|
||||||
|
|
||||||
|
$pager = $query
|
||||||
|
->orderBy('.section, .label')
|
||||||
|
->paginate($page)
|
||||||
|
;
|
||||||
|
|
||||||
|
return $this->render('@Core/setting/setting_admin/index.html.twig', [
|
||||||
|
'pager' => $pager,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/edit/{entity}", name="admin_setting_edit")
|
||||||
|
*/
|
||||||
|
public function edit(
|
||||||
|
Entity $entity,
|
||||||
|
EntityManager $entityManager,
|
||||||
|
EventDispatcherInterface $eventDispatcher,
|
||||||
|
Request $request
|
||||||
|
): Response {
|
||||||
|
$builder = $this->createFormBuilder($entity);
|
||||||
|
$event = new SettingEvent([
|
||||||
|
'builder' => $builder,
|
||||||
|
'entity' => $entity,
|
||||||
|
'options' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$eventDispatcher->dispatch($event, SettingEvent::FORM_INIT_EVENT);
|
||||||
|
|
||||||
|
$form = $builder->getForm();
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$entityManager->update($entity);
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_setting_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/setting/setting_admin/edit.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'entity' => $entity,
|
||||||
|
'options' => $event->getData()['options'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/delete/{entity}", name="admin_setting_delete", methods={"DELETE"})
|
||||||
|
*/
|
||||||
|
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
|
||||||
|
$entityManager->delete($entity);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been removed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_setting_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSection(): string
|
||||||
|
{
|
||||||
|
return 'setting';
|
||||||
|
}
|
||||||
|
}
|
82
core/Controller/Site/MenuAdminController.php
Normal file
82
core/Controller/Site/MenuAdminController.php
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Site;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use App\Core\Entity\Site\Menu as Entity;
|
||||||
|
use App\Core\Entity\Site\Navigation;
|
||||||
|
use App\Core\Factory\Site\MenuFactory as EntityFactory;
|
||||||
|
use App\Core\Form\Site\MenuType as EntityType;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/menu")
|
||||||
|
*/
|
||||||
|
class MenuAdminController extends AdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/new/{navigation}", name="admin_site_menu_new", methods={"POST"})
|
||||||
|
*/
|
||||||
|
public function new(Navigation $navigation, EntityFactory $factory, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
$entity = $factory->create($navigation);
|
||||||
|
$form = $this->createForm(EntityType::class, $entity);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$entityManager->create($entity);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $navigation->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/edit/{entity}", name="admin_site_menu_edit", methods={"POST"})
|
||||||
|
*/
|
||||||
|
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
$form = $this->createForm(EntityType::class, $entity);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$entityManager->update($entity);
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $entity->getNavigation()->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/delete/{entity}", name="admin_site_menu_delete", methods={"DELETE"})
|
||||||
|
*/
|
||||||
|
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
|
||||||
|
$entityManager->delete($entity);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been removed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $entity->getNavigation()->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSection(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
142
core/Controller/Site/NavigationAdminController.php
Normal file
142
core/Controller/Site/NavigationAdminController.php
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Site;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\Crud\CrudController;
|
||||||
|
use App\Core\Crud\CrudConfiguration;
|
||||||
|
use App\Core\Crud\Field;
|
||||||
|
use App\Core\Entity\Site\Navigation as Entity;
|
||||||
|
use App\Core\Event\Setting\NavigationSettingEvent;
|
||||||
|
use App\Core\Factory\Site\NavigationFactory as Factory;
|
||||||
|
use App\Core\Form\Site\NavigationType as Type;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use App\Core\Repository\NavigationSettingRepositoryQuery;
|
||||||
|
use App\Core\Repository\Site\NavigationRepositoryQuery as RepositoryQuery;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
class NavigationAdminController extends CrudController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/navigation/{page}", name="admin_site_navigation_index", methods={"GET"}, requirements={"page":"\d+"})
|
||||||
|
*/
|
||||||
|
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
|
||||||
|
{
|
||||||
|
return $this->doIndex($page, $query, $request, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/navigation/new", name="admin_site_navigation_new", methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doNew($factory->create(), $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/navigation/show/{entity}", name="admin_site_navigation_show", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function show(
|
||||||
|
Entity $entity,
|
||||||
|
EventDispatcherInterface $eventDispatcher,
|
||||||
|
NavigationSettingRepositoryQuery $settingQuery
|
||||||
|
): Response {
|
||||||
|
$eventDispatcher->dispatch(new NavigationSettingEvent([
|
||||||
|
'navigation' => $entity,
|
||||||
|
]), NavigationSettingEvent::INIT_EVENT);
|
||||||
|
|
||||||
|
$settings = $settingQuery
|
||||||
|
->where('.navigation = :navigation')
|
||||||
|
->orderBy('.section, .label')
|
||||||
|
->setParameter(':navigation', $entity->getId())
|
||||||
|
->paginate(1, 1000)
|
||||||
|
;
|
||||||
|
|
||||||
|
$this->getConfiguration()->addViewData('show', 'settings', $settings);
|
||||||
|
|
||||||
|
return $this->doShow($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/navigation/filter", name="admin_site_navigation_filter", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function filter(Session $session): Response
|
||||||
|
{
|
||||||
|
return $this->doFilter($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/navigation/edit/{entity}", name="admin_site_navigation_edit", methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doEdit($entity, $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/navigation/sort/{page}", name="admin_site_navigation_sort", methods={"POST"}, requirements={"page":"\d+"})
|
||||||
|
*/
|
||||||
|
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1, ): Response
|
||||||
|
{
|
||||||
|
return $this->doSort($page, $query, $entityManager, $request, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/navigation/delete/{entity}", name="admin_site_navigation_delete", methods={"DELETE"})
|
||||||
|
*/
|
||||||
|
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doDelete($entity, $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getConfiguration(): CrudConfiguration
|
||||||
|
{
|
||||||
|
return CrudConfiguration::create()
|
||||||
|
->setPageTitle('index', 'Navigations')
|
||||||
|
->setPageTitle('edit', '{label}')
|
||||||
|
->setPageTitle('new', 'New navigation')
|
||||||
|
->setPageTitle('show', '{label}')
|
||||||
|
|
||||||
|
->setPageRoute('index', 'admin_site_navigation_index')
|
||||||
|
->setPageRoute('new', 'admin_site_navigation_new')
|
||||||
|
->setPageRoute('edit', 'admin_site_navigation_edit')
|
||||||
|
->setPageRoute('show', 'admin_site_navigation_show')
|
||||||
|
->setPageRoute('sort', 'admin_site_navigation_sort')
|
||||||
|
->setPageRoute('delete', 'admin_site_navigation_delete')
|
||||||
|
->setPageRoute('filter', 'admin_site_navigation_filter')
|
||||||
|
->setPageRoute('redirects', 'admin_redirect_index')
|
||||||
|
|
||||||
|
->setForm('edit', Type::class, [])
|
||||||
|
->setForm('new', Type::class)
|
||||||
|
|
||||||
|
->setView('index', '@Core/site/navigation_admin/index.html.twig')
|
||||||
|
->setView('show', '@Core/site/navigation_admin/show.html.twig')
|
||||||
|
->setView('show_entity', '@Core/site/navigation_admin/_show.html.twig')
|
||||||
|
->setView('form', '@Core/site/navigation_admin/_form.html.twig')
|
||||||
|
|
||||||
|
->setIsSortableCollection('index', true)
|
||||||
|
|
||||||
|
->setField('index', 'Label', Field\TextField::class, [
|
||||||
|
'property' => 'label',
|
||||||
|
'attr' => ['class' => 'miw-200'],
|
||||||
|
])
|
||||||
|
->setField('index', 'Domain', Field\ButtonField::class, [
|
||||||
|
'property' => 'domain',
|
||||||
|
'button_attr' => ['class' => 'btn btn-light'],
|
||||||
|
'attr' => ['class' => 'miw-200'],
|
||||||
|
])
|
||||||
|
->setField('index', 'Locale', Field\ButtonField::class, [
|
||||||
|
'property' => 'locale',
|
||||||
|
'button_attr' => ['class' => 'btn btn-light'],
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSection(): string
|
||||||
|
{
|
||||||
|
return 'site_navigation';
|
||||||
|
}
|
||||||
|
}
|
305
core/Controller/Site/NodeAdminController.php
Normal file
305
core/Controller/Site/NodeAdminController.php
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Site;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use App\Core\Entity\Site\Node;
|
||||||
|
use App\Core\Entity\Site\Node as Entity;
|
||||||
|
use App\Core\Entity\Site\Page\Page;
|
||||||
|
use App\Core\Event\EntityManager\EntityManagerEvent;
|
||||||
|
use App\Core\Factory\Site\NodeFactory as EntityFactory;
|
||||||
|
use App\Core\Factory\Site\Page\PageFactory;
|
||||||
|
use App\Core\Form\Site\NodeMoveType;
|
||||||
|
use App\Core\Form\Site\NodeType as EntityType;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use App\Core\Repository\Site\NodeRepository;
|
||||||
|
use App\Core\Site\ControllerLocator;
|
||||||
|
use App\Core\Site\PageLocator;
|
||||||
|
use App\Core\Sitemap\SitemapBuilder;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Form\FormError;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/node")
|
||||||
|
*/
|
||||||
|
class NodeAdminController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/new/{node}", name="admin_site_node_new")
|
||||||
|
*/
|
||||||
|
public function new(
|
||||||
|
Node $node,
|
||||||
|
EntityFactory $factory,
|
||||||
|
PageFactory $pageFactory,
|
||||||
|
EntityManager $entityManager,
|
||||||
|
NodeRepository $nodeRepository,
|
||||||
|
PageLocator $pageLocator,
|
||||||
|
ControllerLocator $controllerLocator,
|
||||||
|
Request $request
|
||||||
|
): Response {
|
||||||
|
$entity = $factory->create($node->getMenu());
|
||||||
|
$form = $this->createForm(EntityType::class, $entity, [
|
||||||
|
'pages' => $pageLocator->getPages(),
|
||||||
|
'controllers' => $controllerLocator->getControllers(),
|
||||||
|
'navigation' => $node->getMenu()->getNavigation(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$position = $form->get('position')->getData();
|
||||||
|
|
||||||
|
$parent = 'above' === $position ? $node : $node->getParent();
|
||||||
|
$entity->setParent($parent);
|
||||||
|
|
||||||
|
if ('above' === $position) {
|
||||||
|
$nodeRepository->persistAsLastChild($entity, $node);
|
||||||
|
} else {
|
||||||
|
if ('after' === $position) {
|
||||||
|
$nodeRepository->persistAsNextSiblingOf($entity, $node);
|
||||||
|
} elseif ('before' === $position) {
|
||||||
|
$nodeRepository->persistAsPrevSiblingOf($entity, $node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->handlePageAssociation(
|
||||||
|
$form->get('pageAction')->getData(),
|
||||||
|
$form->get('pageEntity')->getData(),
|
||||||
|
$form->get('pageType')->getData(),
|
||||||
|
$entity,
|
||||||
|
$pageFactory,
|
||||||
|
$pageLocator
|
||||||
|
);
|
||||||
|
|
||||||
|
$entityManager->create($entity);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
|
||||||
|
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $node->getMenu()->getNavigation()->getId(),
|
||||||
|
'data-modal' => $this->generateUrl('admin_site_node_edit', ['entity' => $entity->getId()]),
|
||||||
|
]).sprintf('#node-%d', $entity->getId()));
|
||||||
|
}
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
|
||||||
|
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $entity->getMenu()->getNavigation()->getId(),
|
||||||
|
]).sprintf('#node-%d', $entity->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/site/node_admin/new.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'node' => $node,
|
||||||
|
'entity' => $entity,
|
||||||
|
'tab' => 'content',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/edit/{entity}/{tab}", name="admin_site_node_edit")
|
||||||
|
*/
|
||||||
|
public function edit(
|
||||||
|
Entity $entity,
|
||||||
|
EntityManager $entityManager,
|
||||||
|
PageFactory $pageFactory,
|
||||||
|
PageLocator $pageLocator,
|
||||||
|
ControllerLocator $controllerLocator,
|
||||||
|
Request $request,
|
||||||
|
string $tab = 'content'
|
||||||
|
): Response {
|
||||||
|
$form = $this->createForm(EntityType::class, $entity, [
|
||||||
|
'pages' => $pageLocator->getPages(),
|
||||||
|
'controllers' => $controllerLocator->getControllers(),
|
||||||
|
'navigation' => $entity->getMenu()->getNavigation(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$this->handlePageAssociation(
|
||||||
|
$form->get('pageAction')->getData(),
|
||||||
|
$form->get('pageEntity')->getData(),
|
||||||
|
$form->get('pageType')->getData(),
|
||||||
|
$entity,
|
||||||
|
$pageFactory,
|
||||||
|
$pageLocator
|
||||||
|
);
|
||||||
|
|
||||||
|
$entityManager->update($entity);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $entity->getMenu()->getNavigation()->getId(),
|
||||||
|
'data-modal' => $this->generateUrl('admin_site_node_edit', ['entity' => $entity->getId()]),
|
||||||
|
]).sprintf('#node-%d', $entity->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = $entity->getPage();
|
||||||
|
|
||||||
|
if ($page !== null) {
|
||||||
|
$pageConfiguration = $pageLocator->getPages()[get_class($page)] ?? null;
|
||||||
|
} else {
|
||||||
|
$pageConfiguration = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/site/node_admin/edit.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'entity' => $entity,
|
||||||
|
'page' => $page,
|
||||||
|
'pageConfiguration' => $pageConfiguration,
|
||||||
|
'tab' => $tab,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/urls/{entity}", name="admin_site_node_urls")
|
||||||
|
*/
|
||||||
|
public function urls(Entity $entity, SitemapBuilder $builder): Response
|
||||||
|
{
|
||||||
|
return $this->render('@Core/site/node_admin/urls.html.twig', [
|
||||||
|
'entity' => $entity,
|
||||||
|
'urls' => $builder->getNodeUrls($entity),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/move/{entity}", name="admin_site_node_move")
|
||||||
|
*/
|
||||||
|
public function move(
|
||||||
|
Entity $entity,
|
||||||
|
EntityManager $entityManager,
|
||||||
|
NodeRepository $nodeRepository,
|
||||||
|
Request $request
|
||||||
|
): Response {
|
||||||
|
$form = $this->createForm(NodeMoveType::class, null, [
|
||||||
|
'menu' => $entity->getMenu(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->get('node')->getData()->getId() === $entity->getId()) {
|
||||||
|
$form->get('node')->addError(new FormError('Élement de référence invalide.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form->isValid()) {
|
||||||
|
$position = $form->get('position')->getData();
|
||||||
|
$node = $form->get('node')->getData();
|
||||||
|
|
||||||
|
$parent = 'above' === $position ? $node : $node->getParent();
|
||||||
|
$entity->setParent($parent);
|
||||||
|
|
||||||
|
if ('above' === $position) {
|
||||||
|
$nodeRepository->persistAsLastChild($entity, $node);
|
||||||
|
$entityManager->flush();
|
||||||
|
} else {
|
||||||
|
if ('after' === $position) {
|
||||||
|
$nodeRepository->persistAsNextSiblingOf($entity, $node);
|
||||||
|
} elseif ('before' === $position) {
|
||||||
|
$nodeRepository->persistAsPrevSiblingOf($entity, $node);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $entity->getMenu()->getNavigation()->getId(),
|
||||||
|
]).sprintf('#node-%d', $entity->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/site/node_admin/move.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'entity' => $entity,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/toggle/visibility/{entity}", name="admin_site_node_toggle_visibility", methods={"POST"})
|
||||||
|
*/
|
||||||
|
public function toggleVisibility(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
if ($this->isCsrfTokenValid('toggle_visibility'.$entity->getId(), $request->request->get('_token'))) {
|
||||||
|
$entity->setIsVisible(!$entity->getIsVisible());
|
||||||
|
|
||||||
|
$entityManager->update($entity);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $entity->getMenu()->getNavigation()->getId(),
|
||||||
|
]).sprintf('#node-%d', $entity->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/delete/{entity}", name="admin_site_node_delete", methods={"DELETE"})
|
||||||
|
*/
|
||||||
|
public function delete(
|
||||||
|
Entity $entity,
|
||||||
|
NodeRepository $nodeRepository,
|
||||||
|
EventDispatcherInterface $eventDispatcher,
|
||||||
|
Request $request
|
||||||
|
): Response {
|
||||||
|
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
|
||||||
|
$eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_DELETE_EVENT);
|
||||||
|
$nodeRepository->removeFromTree($entity);
|
||||||
|
$nodeRepository->reorder($entity->getMenu()->getRootNode());
|
||||||
|
$eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Donnée supprimée.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $entity->getMenu()->getNavigation()->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function handlePageAssociation(
|
||||||
|
string $pageAction,
|
||||||
|
?Page $pageEntity,
|
||||||
|
string $pageType,
|
||||||
|
Entity $entity,
|
||||||
|
PageFactory $pageFactory,
|
||||||
|
PageLocator $pageLocator
|
||||||
|
) {
|
||||||
|
if ('new' === $pageAction) {
|
||||||
|
$pageConfiguration = $pageLocator->getPage($pageType);
|
||||||
|
$page = $pageFactory->create($pageType, $entity->getLabel());
|
||||||
|
$page->setTemplate($pageConfiguration->getTemplates()[0]['file']);
|
||||||
|
|
||||||
|
$entity
|
||||||
|
->setPage($page)
|
||||||
|
->setAliasNode(null)
|
||||||
|
;
|
||||||
|
} elseif ('existing' === $pageAction) {
|
||||||
|
if ($pageEntity) {
|
||||||
|
$entity->setPage($pageEntity);
|
||||||
|
} else {
|
||||||
|
$this->addFlash('info', 'Aucun changement de page effectué.');
|
||||||
|
}
|
||||||
|
$entity->setAliasNode(null);
|
||||||
|
} elseif ('alias' === $pageAction) {
|
||||||
|
$entity->setPage(null);
|
||||||
|
} elseif ('none' === $pageAction) {
|
||||||
|
$entity
|
||||||
|
->setPage(null)
|
||||||
|
->setAliasNode(null)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
core/Controller/Site/PageAdminController.php
Normal file
136
core/Controller/Site/PageAdminController.php
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Site;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\Crud\CrudController;
|
||||||
|
use App\Core\Crud\CrudConfiguration;
|
||||||
|
use App\Core\Crud\Field;
|
||||||
|
use App\Core\Entity\Site\Page\Page as Entity;
|
||||||
|
use App\Core\Form\Site\Page\Filter\PageFilterType as FilterType;
|
||||||
|
use App\Core\Form\Site\Page\PageType as Type;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use App\Core\Repository\Site\Page\PageRepositoryQuery as RepositoryQuery;
|
||||||
|
use App\Core\Site\PageLocator;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use App\Core\Event\Page\PageEditEvent;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use App\Core\Entity\EntityInterface;
|
||||||
|
|
||||||
|
class PageAdminController extends CrudController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/page/{page}", name="admin_site_page_index", methods={"GET"}, requirements={"page":"\d+"})
|
||||||
|
*/
|
||||||
|
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
|
||||||
|
{
|
||||||
|
return $this->doIndex($page, $query, $request, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/page/show/{entity}", name="admin_site_page_show", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function show(Entity $entity): Response
|
||||||
|
{
|
||||||
|
return $this->doShow($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/page/filter", name="admin_site_page_filter", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function filter(Session $session): Response
|
||||||
|
{
|
||||||
|
return $this->doFilter($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/page/edit/{entity}", name="admin_site_page_edit", methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function edit(
|
||||||
|
int $entity,
|
||||||
|
EntityManager $entityManager,
|
||||||
|
RepositoryQuery $repositoryQuery,
|
||||||
|
PageLocator $pageLocator,
|
||||||
|
EventDispatcherInterface $eventDispatcher,
|
||||||
|
Request $request
|
||||||
|
): Response {
|
||||||
|
$entity = $repositoryQuery->filterById($entity)->findOne();
|
||||||
|
|
||||||
|
$event = new PageEditEvent($entity);
|
||||||
|
$eventDispatcher->dispatch($event, PageEditEvent::FORM_INIT_EVENT);
|
||||||
|
|
||||||
|
$this->getConfiguration()->setFormOptions('edit', [
|
||||||
|
'page_configuration' => $pageLocator->getPage(get_class($entity)),
|
||||||
|
'page_builder_options' => $event->getPageBuilderOptions(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->doEdit($entity, $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/page/delete/{entity}", name="admin_site_page_delete", methods={"DELETE"})
|
||||||
|
*/
|
||||||
|
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doDelete($entity, $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/page/batch/{page}", name="admin_site_page_batch", methods={"POST"}, requirements={"page":"\d+"})
|
||||||
|
*/
|
||||||
|
public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
|
||||||
|
{
|
||||||
|
return $this->doBatch($page, $query, $entityManager, $request, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getConfiguration(): CrudConfiguration
|
||||||
|
{
|
||||||
|
return CrudConfiguration::create()
|
||||||
|
->setPageTitle('index', 'Pages')
|
||||||
|
->setPageTitle('edit', '{name}')
|
||||||
|
->setPageTitle('show', '{name}')
|
||||||
|
|
||||||
|
->setPageRoute('index', 'admin_site_page_index')
|
||||||
|
->setPageRoute('edit', 'admin_site_page_edit')
|
||||||
|
->setPageRoute('delete', 'admin_site_page_delete')
|
||||||
|
->setPageRoute('filter', 'admin_site_page_filter')
|
||||||
|
->setPageRoute('batch', 'admin_site_page_batch')
|
||||||
|
|
||||||
|
->setForm('edit', Type::class, [])
|
||||||
|
->setForm('filter', FilterType::class)
|
||||||
|
->setView('form', '@Core/site/page_admin/_form.html.twig')
|
||||||
|
|
||||||
|
->setAction('index', 'new', false)
|
||||||
|
->setAction('index', 'show', false)
|
||||||
|
->setAction('edit', 'show', false)
|
||||||
|
|
||||||
|
->setField('index', 'Name', Field\TextField::class, [
|
||||||
|
'property' => 'name',
|
||||||
|
'sort' => ['name', '.name'],
|
||||||
|
'attr' => ['class' => 'col-4'],
|
||||||
|
])
|
||||||
|
->setField('index', 'Elements', Field\TextField::class, [
|
||||||
|
'view' => '@Core/site/page_admin/fields/nodes.html.twig',
|
||||||
|
'sort' => ['navigation', function (RepositoryQuery $query, $direction) {
|
||||||
|
$query
|
||||||
|
->leftJoin('.nodes', 'node')
|
||||||
|
->leftJoin('node.menu', 'menu')
|
||||||
|
->leftJoin('menu.navigation', 'navigation')
|
||||||
|
->orderBy('navigation.label', $direction)
|
||||||
|
;
|
||||||
|
}],
|
||||||
|
'attr' => ['class' => 'col-6'],
|
||||||
|
])
|
||||||
|
->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {
|
||||||
|
$manager->delete($entity);
|
||||||
|
})
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSection(): string
|
||||||
|
{
|
||||||
|
return 'site_page';
|
||||||
|
}
|
||||||
|
}
|
57
core/Controller/Site/PageController.php
Normal file
57
core/Controller/Site/PageController.php
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Site;
|
||||||
|
|
||||||
|
use App\Core\Site\SiteRequest;
|
||||||
|
use App\Core\Site\SiteStore;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class PageController extends AbstractController
|
||||||
|
{
|
||||||
|
protected SiteRequest $siteRequest;
|
||||||
|
protected SiteStore $siteStore;
|
||||||
|
|
||||||
|
public function __construct(SiteRequest $siteRequest, SiteStore $siteStore)
|
||||||
|
{
|
||||||
|
$this->siteRequest = $siteRequest;
|
||||||
|
$this->siteStore = $siteStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(): Response
|
||||||
|
{
|
||||||
|
if (!$this->siteRequest->getPage()) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->defaultRender($this->siteRequest->getPage()->getTemplate());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function defaultRender(string $view, array $parameters = [], Response $response = null): Response
|
||||||
|
{
|
||||||
|
$parameters = array_merge($this->getDefaultRenderParameters(), $parameters);
|
||||||
|
|
||||||
|
if (null === $response) {
|
||||||
|
$contentType = $this->siteRequest->getNode()->getContentType();
|
||||||
|
|
||||||
|
$response = new Response(null, 200, [
|
||||||
|
'Content-Type' => $contentType ?? 'text/html',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::render($view, $parameters, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaultRenderParameters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'_node' => $this->siteRequest->getNode(),
|
||||||
|
'_page' => $this->siteRequest->getPage(),
|
||||||
|
'_menu' => $this->siteRequest->getMenu(),
|
||||||
|
'_navigation' => $this->siteRequest->getNavigation(),
|
||||||
|
'_domain' => $this->siteRequest->getDomain(),
|
||||||
|
'_locale' => $this->siteRequest->getNavigation()->getLocale(),
|
||||||
|
'_store' => $this->siteStore,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
40
core/Controller/Site/SitemapController.php
Normal file
40
core/Controller/Site/SitemapController.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Site;
|
||||||
|
|
||||||
|
use App\Core\Repository\Site\NavigationRepositoryQuery;
|
||||||
|
use App\Core\Sitemap\SitemapBuilder;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
class SitemapController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/sitemap.xml", name="sitemap")
|
||||||
|
*/
|
||||||
|
public function sitemap(Request $request, NavigationRepositoryQuery $navigationRepositoryQuery, SitemapBuilder $builder): Response
|
||||||
|
{
|
||||||
|
$navigations = $navigationRepositoryQuery
|
||||||
|
->whereDomain($request->getHost())
|
||||||
|
->find()
|
||||||
|
;
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
foreach ($navigations as $navigation) {
|
||||||
|
$items = array_merge(
|
||||||
|
$items,
|
||||||
|
$builder->build($navigation)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new Response();
|
||||||
|
$response->headers->set('Content-Type', 'text/xml');
|
||||||
|
|
||||||
|
return $this->render('@Core/site/sitemap/sitemap.xml.twig', [
|
||||||
|
'items' => $items,
|
||||||
|
], $response);
|
||||||
|
}
|
||||||
|
}
|
87
core/Controller/Site/TreeAdminController.php
Normal file
87
core/Controller/Site/TreeAdminController.php
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Site;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use App\Core\Entity\Site\Navigation;
|
||||||
|
use App\Core\Factory\Site\MenuFactory;
|
||||||
|
use App\Core\Form\Site\MenuType;
|
||||||
|
use App\Core\Repository\Site\NavigationRepositoryQuery;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/site/tree")
|
||||||
|
*/
|
||||||
|
class TreeAdminController extends AdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/", name="admin_site_tree_index")
|
||||||
|
*/
|
||||||
|
public function index(NavigationRepositoryQuery $navigationQuery, Session $session): Response
|
||||||
|
{
|
||||||
|
$navigation = null;
|
||||||
|
|
||||||
|
if ($session->has('site_tree_last_navigation')) {
|
||||||
|
$navigation = $navigationQuery->create()
|
||||||
|
->filterById((int) $session->get('site_tree_last_navigation'))
|
||||||
|
->findOne()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $navigation) {
|
||||||
|
$navigation = $navigationQuery->create()
|
||||||
|
->orderBy('.sortOrder')
|
||||||
|
->findOne()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $navigation) {
|
||||||
|
$this->addFlash('warning', 'You must add a navigation.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_site_navigation_new');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_site_tree_navigation', [
|
||||||
|
'navigation' => $navigation->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/navigation/{navigation}", name="admin_site_tree_navigation")
|
||||||
|
*/
|
||||||
|
public function navigation(
|
||||||
|
Navigation $navigation,
|
||||||
|
NavigationRepositoryQuery $navigationQuery,
|
||||||
|
MenuFactory $menuFactory,
|
||||||
|
Session $session
|
||||||
|
): Response {
|
||||||
|
$navigations = $navigationQuery->create()
|
||||||
|
->orderBy('.sortOrder')
|
||||||
|
->find()
|
||||||
|
;
|
||||||
|
|
||||||
|
$session->set('site_tree_last_navigation', $navigation->getId());
|
||||||
|
|
||||||
|
$forms = [
|
||||||
|
'menu' => $this->createForm(MenuType::class, $menuFactory->create())->createView(),
|
||||||
|
'menus' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($navigation->getMenus() as $menu) {
|
||||||
|
$forms['menus'][$menu->getId()] = $this->createForm(MenuType::class, $menu)->createView();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@Core/site/tree_admin/navigation.html.twig', [
|
||||||
|
'navigation' => $navigation,
|
||||||
|
'navigations' => $navigations,
|
||||||
|
'forms' => $forms,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSection(): string
|
||||||
|
{
|
||||||
|
return 'site_tree';
|
||||||
|
}
|
||||||
|
}
|
62
core/Controller/Task/TaskAdminController.php
Normal file
62
core/Controller/Task/TaskAdminController.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Task;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\AdminController;
|
||||||
|
use App\Core\Event\Task\TaskInitEvent;
|
||||||
|
use App\Core\Event\Task\TaskRunRequestedEvent;
|
||||||
|
use SensioLabs\AnsiConverter\AnsiToHtmlConverter;
|
||||||
|
use SensioLabs\AnsiConverter\Theme\SolarizedTheme;
|
||||||
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/task")
|
||||||
|
*/
|
||||||
|
class TaskAdminController extends AdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/", name="admin_task_index")
|
||||||
|
*/
|
||||||
|
public function index(EventDispatcherInterface $eventDispatcher): Response
|
||||||
|
{
|
||||||
|
$event = new TaskInitEvent();
|
||||||
|
$eventDispatcher->dispatch($event, TaskInitEvent::INIT_EVENT);
|
||||||
|
|
||||||
|
return $this->render('@Core/task/task_admin/index.html.twig', [
|
||||||
|
'pager' => $event->getTasks(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/run/{task}", name="admin_task_run", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function run(
|
||||||
|
string $task,
|
||||||
|
Request $request,
|
||||||
|
EventDispatcherInterface $eventDispatcher
|
||||||
|
): Response {
|
||||||
|
if (!$this->isCsrfTokenValid('task_run', $request->query->get('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = new BufferedOutput();
|
||||||
|
$event = new TaskRunRequestedEvent($task, $request->query, $output);
|
||||||
|
$eventDispatcher->dispatch($event, TaskRunRequestedEvent::RUN_REQUEST_EVENT);
|
||||||
|
|
||||||
|
$converter = new AnsiToHtmlConverter(new SolarizedTheme());
|
||||||
|
$content = $converter->convert($output->fetch());
|
||||||
|
|
||||||
|
return $this->render('@Core/task/task_admin/run.html.twig', [
|
||||||
|
'output' => $content,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSection(): string
|
||||||
|
{
|
||||||
|
return 'task';
|
||||||
|
}
|
||||||
|
}
|
132
core/Controller/User/UserAdminController.php
Normal file
132
core/Controller/User/UserAdminController.php
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\User;
|
||||||
|
|
||||||
|
use App\Core\Controller\Admin\Crud\CrudController;
|
||||||
|
use App\Core\Crud\CrudConfiguration;
|
||||||
|
use App\Core\Crud\Field;
|
||||||
|
use App\Core\Event\Account\PasswordRequestEvent;
|
||||||
|
use App\Core\Factory\UserFactory as Factory;
|
||||||
|
use App\Core\Manager\EntityManager;
|
||||||
|
use App\Entity\User as Entity;
|
||||||
|
use App\Form\UserType as Type;
|
||||||
|
use App\Repository\UserRepositoryQuery as RepositoryQuery;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
class UserAdminController extends CrudController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/admin/user/{page}", name="admin_user_index", methods={"GET"}, requirements={"page":"\d+"})
|
||||||
|
*/
|
||||||
|
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
|
||||||
|
{
|
||||||
|
return $this->doIndex($page, $query, $request, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/user/new", name="admin_user_new", methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
$entity = $factory->create($this->getUser());
|
||||||
|
|
||||||
|
return $this->doNew($factory->create(), $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/user/show/{entity}", name="admin_user_show", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function show(Entity $entity): Response
|
||||||
|
{
|
||||||
|
return $this->doShow($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/user/filter", name="admin_user_filter", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function filter(Session $session): Response
|
||||||
|
{
|
||||||
|
return $this->doFilter($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/user/edit/{entity}", name="admin_user_edit", methods={"GET", "POST"})
|
||||||
|
*/
|
||||||
|
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doEdit($entity, $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/user/delete/{entity}", name="admin_user_delete", methods={"DELETE"})
|
||||||
|
*/
|
||||||
|
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doDelete($entity, $entityManager, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/user/resetting_request/{entity}", name="admin_user_resetting_request", methods={"POST"})
|
||||||
|
*/
|
||||||
|
public function requestResetting(Entity $entity, EventDispatcherInterface $eventDispatcher, Request $request): Response
|
||||||
|
{
|
||||||
|
if ($this->isCsrfTokenValid('resetting_request'.$entity->getId(), $request->request->get('_token'))) {
|
||||||
|
$eventDispatcher->dispatch(new PasswordRequestEvent($entity), PasswordRequestEvent::EVENT);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'E-mail sent.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_user_edit', [
|
||||||
|
'entity' => $entity->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getConfiguration(): CrudConfiguration
|
||||||
|
{
|
||||||
|
return CrudConfiguration::create()
|
||||||
|
->setPageTitle('index', 'Users')
|
||||||
|
->setPageTitle('edit', '{username}')
|
||||||
|
->setPageTitle('new', 'New user')
|
||||||
|
->setPageTitle('show', '{username}')
|
||||||
|
|
||||||
|
->setPageRoute('index', 'admin_user_index')
|
||||||
|
->setPageRoute('new', 'admin_user_new')
|
||||||
|
->setPageRoute('edit', 'admin_user_edit')
|
||||||
|
->setPageRoute('show', 'admin_user_show')
|
||||||
|
->setPageRoute('delete', 'admin_user_delete')
|
||||||
|
->setPageRoute('filter', 'admin_user_filter')
|
||||||
|
|
||||||
|
->setForm('edit', Type::class, [])
|
||||||
|
->setForm('new', Type::class)
|
||||||
|
|
||||||
|
->setView('form', '@Core/user/user_admin/_form.html.twig')
|
||||||
|
->setView('index', '@Core/user/user_admin/index.html.twig')
|
||||||
|
->setView('new', '@Core/user/user_admin/new.html.twig')
|
||||||
|
->setView('show', '@Core/user/user_admin/show.html.twig')
|
||||||
|
->setView('show_entity', '@Core/user/user_admin/_show.html.twig')
|
||||||
|
->setView('edit', '@Core/user/user_admin/edit.html.twig')
|
||||||
|
|
||||||
|
->setDefaultSort('index', 'username')
|
||||||
|
|
||||||
|
->setField('index', 'E-mail', Field\TextField::class, [
|
||||||
|
'property' => 'email',
|
||||||
|
'sort' => ['email', '.email'],
|
||||||
|
'attr' => ['class' => 'miw-200'],
|
||||||
|
])
|
||||||
|
->setField('index', 'Display name', Field\TextField::class, [
|
||||||
|
'property' => 'displayName',
|
||||||
|
'sort' => ['displayName', '.displayName'],
|
||||||
|
'attr' => ['class' => 'miw-200'],
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSection(): string
|
||||||
|
{
|
||||||
|
return 'user';
|
||||||
|
}
|
||||||
|
}
|
330
core/Crud/CrudConfiguration.php
Normal file
330
core/Crud/CrudConfiguration.php
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Crud;
|
||||||
|
|
||||||
|
use App\Core\Crud\Exception\CrudConfigurationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class CrudConfiguration.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class CrudConfiguration
|
||||||
|
{
|
||||||
|
protected array $pageTitles = [];
|
||||||
|
protected array $pageRoutes = [];
|
||||||
|
protected array $pageRouteParams = [];
|
||||||
|
protected array $actions = [];
|
||||||
|
protected array $batchActions = [];
|
||||||
|
protected array $actionTitles = [];
|
||||||
|
protected array $forms = [];
|
||||||
|
protected array $formOptions = [];
|
||||||
|
protected array $views = [];
|
||||||
|
protected array $viewDatas = [];
|
||||||
|
protected array $fields = [];
|
||||||
|
protected array $maxPerPage = [];
|
||||||
|
protected array $locales = [];
|
||||||
|
protected array $defaultSort = [];
|
||||||
|
protected array $isSortableCollection = [];
|
||||||
|
protected string $sortableCollectionProperty = 'sortOrder';
|
||||||
|
protected ?string $defaultLocale = null;
|
||||||
|
protected bool $showActions = true;
|
||||||
|
|
||||||
|
protected static $self;
|
||||||
|
|
||||||
|
public static function create()
|
||||||
|
{
|
||||||
|
if (null === self::$self) {
|
||||||
|
self::$self = new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setPageTitle(string $page, string $title): self
|
||||||
|
{
|
||||||
|
$this->pageTitles[$page] = $title;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPageTitle(string $page, ?string $default = null): ?string
|
||||||
|
{
|
||||||
|
return $this->pageTitles[$page] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setPageRoute(string $page, string $route): self
|
||||||
|
{
|
||||||
|
$this->pageRoutes[$page] = $route;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPageRoute(string $page): ?string
|
||||||
|
{
|
||||||
|
return $this->pageRoutes[$page];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPageRouteParams(string $page, array $params): self
|
||||||
|
{
|
||||||
|
$this->pageRouteParams[$page] = $params;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPageRouteParams(string $page): array
|
||||||
|
{
|
||||||
|
return $this->pageRouteParams[$page] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setForm(string $context, string $form, array $options = []): self
|
||||||
|
{
|
||||||
|
$this->forms[$context] = $form;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getForm(string $context): ?string
|
||||||
|
{
|
||||||
|
return $this->forms[$context] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFormOptions(string $context, array $options = []): self
|
||||||
|
{
|
||||||
|
$this->formOptions[$context] = $options;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormOptions(string $context): array
|
||||||
|
{
|
||||||
|
return $this->formOptions[$context] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setAction(string $page, string $action, bool $enabled): self
|
||||||
|
{
|
||||||
|
if (!isset($this->actions[$page])) {
|
||||||
|
$this->actions[$page] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->actions[$page][$action] = $enabled;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAction(string $page, string $action, bool $default = true)
|
||||||
|
{
|
||||||
|
return $this->actions[$page][$action] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBatchAction(string $page, string $action, string $label, callable $callback): self
|
||||||
|
{
|
||||||
|
if (!isset($this->batchActions[$page])) {
|
||||||
|
$this->batchActions[$page] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->batchActions[$page][$action] = [
|
||||||
|
'label' => $label,
|
||||||
|
'callback' => $callback,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBatchActions(string $page)
|
||||||
|
{
|
||||||
|
return $this->batchActions[$page] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBatchAction(string $page, string $action)
|
||||||
|
{
|
||||||
|
return $this->batchActions[$page][$action] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasBatchAction(string $page)
|
||||||
|
{
|
||||||
|
return !empty($this->batchActions[$page]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setActionTitle(string $page, string $action, string $title): self
|
||||||
|
{
|
||||||
|
if (!isset($this->actionTitles[$page])) {
|
||||||
|
$this->actionTitles[$page] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->actions[$page][$action] = $title;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActionTitle(string $page, string $action, ?string $default = null): ?string
|
||||||
|
{
|
||||||
|
return $this->actionTitles[$page][$action] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setView(string $context, string $view): self
|
||||||
|
{
|
||||||
|
$this->views[$context] = $view;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getView(string $context, ?string $default = null)
|
||||||
|
{
|
||||||
|
if (null === $default) {
|
||||||
|
$default = sprintf('@Core/admin/crud/%s.html.twig', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->views[$context] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addViewData(string $context, string $name, $value): self
|
||||||
|
{
|
||||||
|
if (!isset($this->viewDatas[$context])) {
|
||||||
|
$this->viewDatas[$context] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->viewDatas[$context][$name] = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setViewDatas(string $context, array $datas): self
|
||||||
|
{
|
||||||
|
foreach ($datas as $name => $value) {
|
||||||
|
$this->addViewData($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getViewDatas(string $context): array
|
||||||
|
{
|
||||||
|
return $this->viewDatas[$context] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setField(string $context, string $label, string $field, array $options): self
|
||||||
|
{
|
||||||
|
if (!isset($this->fields[$context])) {
|
||||||
|
$this->fields[$context] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fields[$context][$label] = [
|
||||||
|
'field' => $field,
|
||||||
|
'options' => $options,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFields(string $context): array
|
||||||
|
{
|
||||||
|
return $this->fields[$context] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setMaxPerPage(string $page, int $max)
|
||||||
|
{
|
||||||
|
$this->maxPerPage[$page] = $max;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMaxPerPage(string $page, int $default = 20)
|
||||||
|
{
|
||||||
|
return $this->maxPerPage[$page] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setI18n(array $locales, string $defaultLocale): self
|
||||||
|
{
|
||||||
|
$this->locales = $locales;
|
||||||
|
$this->defaultLocale = $defaultLocale;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocales(): array
|
||||||
|
{
|
||||||
|
return $this->locales;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultLocale(): ?string
|
||||||
|
{
|
||||||
|
return $this->defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isI18n(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->locales);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- */
|
||||||
|
|
||||||
|
public function setDefaultSort(string $context, string $label, string $direction = 'asc'): self
|
||||||
|
{
|
||||||
|
$this->defaultSort[$context] = [
|
||||||
|
'label' => $label,
|
||||||
|
'direction' => $direction,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultSort(string $context)
|
||||||
|
{
|
||||||
|
return $this->defaultSort[$context] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsSortableCollection(string $page, bool $isSortableCollection): self
|
||||||
|
{
|
||||||
|
$this->isSortableCollection[$page] = $isSortableCollection;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIsSortableCollection(string $page): bool
|
||||||
|
{
|
||||||
|
return $this->isSortableCollection[$page] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSortableCollectionProperty(string $sortableCollectionProperty): self
|
||||||
|
{
|
||||||
|
$this->sortableCollectionProperty = $sortableCollectionProperty;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSortableCollectionProperty(): string
|
||||||
|
{
|
||||||
|
return $this->sortableCollectionProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShowActions(bool $showActions): self
|
||||||
|
{
|
||||||
|
$this->showActions = $showActions;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShowActions(): bool
|
||||||
|
{
|
||||||
|
return $this->showActions;
|
||||||
|
}
|
||||||
|
}
|
12
core/Crud/Exception/CrudConfigurationException.php
Normal file
12
core/Crud/Exception/CrudConfigurationException.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Crud\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class CrudConfigurationException.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class CrudConfigurationException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
41
core/Crud/Field/ButtonField.php
Normal file
41
core/Crud/Field/ButtonField.php
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Crud\Field;
|
||||||
|
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class ButtonField.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class ButtonField extends Field
|
||||||
|
{
|
||||||
|
public function buildView(Environment $twig, $entity, array $options, ?string $locale = null)
|
||||||
|
{
|
||||||
|
if (isset($options['button_attr_builder']) && is_callable($options['button_attr_builder'])) {
|
||||||
|
$options['button_attr'] = (array) call_user_func($options['button_attr_builder'], $entity, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::buildView($twig, $entity, $options, $locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): OptionsResolver
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'view' => '@Core/admin/crud/field/button.html.twig',
|
||||||
|
'button_attr' => [],
|
||||||
|
'button_attr_builder' => null,
|
||||||
|
'button_tag' => 'button',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolver->setAllowedTypes('button_attr', ['array']);
|
||||||
|
$resolver->setAllowedTypes('button_tag', ['string']);
|
||||||
|
$resolver->setAllowedTypes('button_attr_builder', ['null', 'callable']);
|
||||||
|
|
||||||
|
return $resolver;
|
||||||
|
}
|
||||||
|
}
|
25
core/Crud/Field/DateField.php
Normal file
25
core/Crud/Field/DateField.php
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Crud\Field;
|
||||||
|
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class DateField.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class DateField extends Field
|
||||||
|
{
|
||||||
|
public function configureOptions(OptionsResolver $resolver): OptionsResolver
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'view' => '@Core/admin/crud/field/date.html.twig',
|
||||||
|
'format' => 'Y-m-d',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $resolver;
|
||||||
|
}
|
||||||
|
}
|
25
core/Crud/Field/DatetimeField.php
Normal file
25
core/Crud/Field/DatetimeField.php
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Crud\Field;
|
||||||
|
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class DatetimeField.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class DatetimeField extends Field
|
||||||
|
{
|
||||||
|
public function configureOptions(OptionsResolver $resolver): OptionsResolver
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'view' => '@Core/admin/crud/field/date.html.twig',
|
||||||
|
'format' => 'Y-m-d H:i:s',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $resolver;
|
||||||
|
}
|
||||||
|
}
|
92
core/Crud/Field/Field.php
Normal file
92
core/Crud/Field/Field.php
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Crud\Field;
|
||||||
|
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
|
||||||
|
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class Field.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
abstract class Field
|
||||||
|
{
|
||||||
|
public function buildView(Environment $twig, $entity, array $options, ?string $locale = null)
|
||||||
|
{
|
||||||
|
return $twig->render($this->getView($options), [
|
||||||
|
'entity' => $entity,
|
||||||
|
'value' => $this->getValue($entity, $options, $locale),
|
||||||
|
'options' => $options,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): OptionsResolver
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'property' => null,
|
||||||
|
'property_builder' => null,
|
||||||
|
'view' => null,
|
||||||
|
'raw' => false,
|
||||||
|
'sort' => null,
|
||||||
|
'href' => null,
|
||||||
|
'href_attr' => [],
|
||||||
|
'attr' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolver->setRequired('view');
|
||||||
|
$resolver->setAllowedTypes('property', ['null', 'string']);
|
||||||
|
$resolver->setAllowedTypes('view', 'string');
|
||||||
|
$resolver->setAllowedTypes('attr', 'array');
|
||||||
|
$resolver->setAllowedTypes('href', ['null', 'string', 'callable']);
|
||||||
|
$resolver->setAllowedTypes('href_attr', 'array', 'callable');
|
||||||
|
$resolver->setAllowedTypes('raw', 'boolean');
|
||||||
|
$resolver->setAllowedTypes('property_builder', ['null', 'callable']);
|
||||||
|
$resolver->setAllowedValues('sort', function($value) {
|
||||||
|
if ($value === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isValidParam1 = !empty($value[0]) && is_string($value[0]);
|
||||||
|
$isValidParam2 = !empty($value[1]) && (is_string($value[1]) || is_callable($value[1]));
|
||||||
|
|
||||||
|
return $isValidParam1 && $isValidParam2;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getValue($entity, array $options, ?string $locale = null)
|
||||||
|
{
|
||||||
|
if (null !== $options['property']) {
|
||||||
|
$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()->getPropertyAccessor();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$value = $propertyAccessor->getValue($entity, $options['property']);
|
||||||
|
} catch (NoSuchPropertyException $e) {
|
||||||
|
if (null !== $locale) {
|
||||||
|
$value = $propertyAccessor->getValue($entity->translate($locale), $options['property']);
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (null !== $options['property_builder']) {
|
||||||
|
$value = call_user_func($options['property_builder'], $entity, $options);
|
||||||
|
} else {
|
||||||
|
$value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getView(array $options)
|
||||||
|
{
|
||||||
|
return $options['view'];
|
||||||
|
}
|
||||||
|
}
|
27
core/Crud/Field/ImageField.php
Normal file
27
core/Crud/Field/ImageField.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Crud\Field;
|
||||||
|
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class ImageField.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class ImageField extends Field
|
||||||
|
{
|
||||||
|
public function configureOptions(OptionsResolver $resolver): OptionsResolver
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'view' => '@Core/admin/crud/field/image.html.twig',
|
||||||
|
'image_attr' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolver->setAllowedTypes('image_attr', ['array']);
|
||||||
|
|
||||||
|
return $resolver;
|
||||||
|
}
|
||||||
|
}
|
24
core/Crud/Field/TextField.php
Normal file
24
core/Crud/Field/TextField.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Crud\Field;
|
||||||
|
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class TextField.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class TextField extends Field
|
||||||
|
{
|
||||||
|
public function configureOptions(OptionsResolver $resolver): OptionsResolver
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'view' => '@Core/admin/crud/field/text.html.twig',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $resolver;
|
||||||
|
}
|
||||||
|
}
|
122
core/DependencyInjection/Configuration.php
Normal file
122
core/DependencyInjection/Configuration.php
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\DependencyInjection;
|
||||||
|
|
||||||
|
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||||
|
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||||
|
|
||||||
|
class Configuration implements ConfigurationInterface
|
||||||
|
{
|
||||||
|
public function getConfigTreeBuilder(): TreeBuilder
|
||||||
|
{
|
||||||
|
$defaultMimetypes = [
|
||||||
|
'image/png',
|
||||||
|
'image/jpg',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/gif',
|
||||||
|
'image/svg+xml',
|
||||||
|
'video/mp4',
|
||||||
|
'audio/mpeg3',
|
||||||
|
'audio/x-mpeg-3',
|
||||||
|
'multipart/x-zip',
|
||||||
|
'multipart/x-gzip',
|
||||||
|
'application/pdf',
|
||||||
|
'application/ogg',
|
||||||
|
'application/zip',
|
||||||
|
'application/rar',
|
||||||
|
'application/x-rar-compressed',
|
||||||
|
'application/x-zip-compressed',
|
||||||
|
'application/tar',
|
||||||
|
'application/x-tar',
|
||||||
|
'application/x-bzip',
|
||||||
|
'application/x-bzip2',
|
||||||
|
'application/x-gzip',
|
||||||
|
'application/octet-stream',
|
||||||
|
'application/msword',
|
||||||
|
'text/plain',
|
||||||
|
'text/css',
|
||||||
|
];
|
||||||
|
|
||||||
|
$defaultLocked = [
|
||||||
|
'%kernel.project_dir%/public/uploads',
|
||||||
|
];
|
||||||
|
|
||||||
|
$treeBuilder = new TreeBuilder('core');
|
||||||
|
|
||||||
|
$treeBuilder->getRootNode()
|
||||||
|
->children()
|
||||||
|
->arrayNode('site')
|
||||||
|
->children()
|
||||||
|
->scalarNode('name')
|
||||||
|
->defaultValue('Murph')
|
||||||
|
->isRequired()
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->scalarNode('logo')
|
||||||
|
->defaultValue('build/images/core/logo.svg')
|
||||||
|
->isRequired()
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->arrayNode('controllers')
|
||||||
|
->prototype('array')
|
||||||
|
->children()
|
||||||
|
->scalarNode('name')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->scalarNode('action')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->arrayNode('pages')
|
||||||
|
->prototype('array')
|
||||||
|
->children()
|
||||||
|
->scalarNode('name')
|
||||||
|
->isRequired()
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->arrayNode('templates')
|
||||||
|
->prototype('array')
|
||||||
|
->children()
|
||||||
|
->scalarNode('name')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->scalarNode('file')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->arrayNode('file_manager')
|
||||||
|
->children()
|
||||||
|
->arrayNode('mimes')
|
||||||
|
->scalarPrototype()
|
||||||
|
->end()
|
||||||
|
->defaultValue($defaultMimetypes)
|
||||||
|
->end()
|
||||||
|
->scalarNode('path')
|
||||||
|
->defaultValue('%kernel.project_dir%/public/uploads')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->scalarNode('path_uri')
|
||||||
|
->defaultValue('/uploads')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->arrayNode('path_locked')
|
||||||
|
->scalarPrototype()
|
||||||
|
->end()
|
||||||
|
->defaultValue($defaultLocked)
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
return $treeBuilder;
|
||||||
|
}
|
||||||
|
}
|
28
core/DependencyInjection/CoreExtension.php
Normal file
28
core/DependencyInjection/CoreExtension.php
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\DependencyInjection;
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
|
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||||
|
|
||||||
|
class CoreExtension extends Extension
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function load(array $configs, ContainerBuilder $container)
|
||||||
|
{
|
||||||
|
$configuration = $this->getConfiguration($configs, $container);
|
||||||
|
$config = $this->processConfiguration($configuration, $configs);
|
||||||
|
|
||||||
|
$container->setParameter('core', $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getConfiguration(array $configs, ContainerBuilder $container)
|
||||||
|
{
|
||||||
|
return new Configuration();
|
||||||
|
}
|
||||||
|
}
|
59
core/Doctrine/Timestampable.php
Normal file
59
core/Doctrine/Timestampable.php
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Doctrine;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
trait Timestampable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @ORM\Column(name="created_at", type="datetime")
|
||||||
|
*/
|
||||||
|
protected $createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(name="updated_at", type="datetime")
|
||||||
|
*/
|
||||||
|
protected $updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\PrePersist
|
||||||
|
*/
|
||||||
|
public function onPrePersist(): void
|
||||||
|
{
|
||||||
|
$this->createdAt = new \DateTime();
|
||||||
|
$this->updatedAt = new \DateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\PreUpdate
|
||||||
|
*/
|
||||||
|
public function onPreUpdate(): void
|
||||||
|
{
|
||||||
|
$this->updatedAt = new \DateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(?\DateTime $createdAt): self
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): ?\DateTime
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUpdatedAt(?\DateTime $updatedAt): self
|
||||||
|
{
|
||||||
|
$this->updatedAt = $updatedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): ?\DateTime
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
}
|
0
core/Entity/.gitignore
vendored
Normal file
0
core/Entity/.gitignore
vendored
Normal file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue