Compare commits
2 Commits
master
...
2_symfony_
Author | SHA1 | Date | |
---|---|---|---|
04e410c133 | |||
92359d6292 |
10
.gitignore
vendored
10
.gitignore
vendored
@ -3,11 +3,5 @@
|
||||
!/app/.keep
|
||||
|
||||
# Uncomment to ignore Database datas
|
||||
data
|
||||
|
||||
# yarn
|
||||
/app/.yarncache
|
||||
|
||||
|
||||
# phpstorm
|
||||
.idea
|
||||
/data/*
|
||||
!/data/.keep
|
||||
|
113
README.md
113
README.md
@ -1,111 +1,26 @@
|
||||
Environnement de développement Docker pour démarrer un nouveau projet Symfony5
|
||||
==================================
|
||||
|
||||
## En partant de la branche *5_start_new-project*
|
||||
# Présentation
|
||||
|
||||
Le projet, symfony et ses dépendances sont prêt à être installé.
|
||||
L'objectif de ce dépôt est de proposer et de faire évoluer une configuration de départ pour démarrer très simplement un nouveau projet Symfony dans des conteneurs Docker.
|
||||
|
||||
```
|
||||
$ docker-compose build
|
||||
$ docker-compose up -d
|
||||
$ docker-compose exec -u 1000 php bash
|
||||
```
|
||||
En suivant pas-à-pas les instructions à partir du chapitre "Installation", on peut démarrer en quelques minutes un nouveau projet.
|
||||
|
||||
dans le conteneur php :
|
||||
Le dépôt propose plusieurs branches qui peuvent être utilisées selon le point de départ recherché:
|
||||
|
||||
```
|
||||
$ composer install
|
||||
$ bin/console doctrine:schema:create
|
||||
```
|
||||
## 1_docker_ready
|
||||
|
||||
et pour node :
|
||||
La branche `1_docker_ready` fournit juste le docker-compose.yml et les Dockerfile qui permettent de construire les conteneurs.
|
||||
|
||||
```
|
||||
$ bash docker-node.sh yarn install --force
|
||||
$ bash docker-node.sh yarn encore dev-server
|
||||
```
|
||||
* `$ cd my-project-dir`
|
||||
* `$ docker-compose build`
|
||||
* `$ docker-compose up -d`
|
||||
* `$ docker-compose exec -u 1000 php bash`
|
||||
|
||||
Voilà, le site est disponible sur http://localhost:8000
|
||||
A ce stade les commandes `composer` et `symfony` sont disponibles pour lancer la création du projet.
|
||||
Après ça on choisira dans le fichier `app/.env` le type de base de donnée. Un conteneur est prévu pour utiliser postgresql.
|
||||
|
||||
## 2_symfony_demo
|
||||
|
||||
## En partant de la branche *1_docker_ready*
|
||||
|
||||
### 1. Se mettre sur la bonne branche
|
||||
|
||||
```bash
|
||||
$ git co -b 1_docker_ready origin/1_docker_ready
|
||||
```
|
||||
|
||||
### 2. Construire les images et les conteneurs
|
||||
|
||||
```bash
|
||||
$ docker-compose build
|
||||
$ docker-compose up
|
||||
.. 221001_test3_db_1 exited with code 1
|
||||
```
|
||||
|
||||
### 3. Parce que 'db' ne se lance pas:
|
||||
|
||||
```bash
|
||||
$ docker-compose rm db
|
||||
$ sudo rm -rf ./data
|
||||
$ docker-compose up db
|
||||
```
|
||||
|
||||
### 4. Entrer dans le conteneur php
|
||||
|
||||
```bash
|
||||
$ docker-compose exec -u 1000 php bash
|
||||
```
|
||||
|
||||
### 5. Créer le projet
|
||||
|
||||
```bash
|
||||
$ symfony new project
|
||||
$ mv project/* . && mv project/.* . && rmdir project
|
||||
```
|
||||
|
||||
### 6. Charger les dépendances de composer
|
||||
|
||||
```bash
|
||||
$ composer require doctrine/annotations twig/twig doctrine/orm symfony/orm-pack symfony/form symfony/maker-bundle symfony/security-csrf
|
||||
```
|
||||
|
||||
### 7. Connexion à postgresql
|
||||
|
||||
modifier DATABASE_URL dans app/.env :
|
||||
```bash
|
||||
+++ DATABASE_URL="postgresql://postgres:secret@db:5432/postgres?serverVersion=12&charset=utf8"
|
||||
```
|
||||
|
||||
et dans le conteneur php :
|
||||
```bash
|
||||
$ bin/console doctrine:schema:create
|
||||
```
|
||||
|
||||
### 8. Chargement de dépendances, la suite
|
||||
|
||||
```bash
|
||||
$ composer require symfony/yaml symfony/twig-bridge symfony/validator
|
||||
|
||||
$ composer require symfony/asset symfony/expression-language symfony/security-http symfony/translation symfony/web-link egulias/email-validator symfony/expression-language symfony/intl symfony/translation
|
||||
|
||||
$ composer require --dev symfony/profiler-pack symfony/debug-bundle symfony/var-dumper
|
||||
```
|
||||
|
||||
### 9. Installer node et yarn
|
||||
|
||||
```bash
|
||||
$ composer require symfony/webpack-encore-bundle
|
||||
```
|
||||
|
||||
mettre en place le script qui lance docker node, le lancer pour entrer dans le conteneur node:
|
||||
```bash
|
||||
$ yarn install --force
|
||||
$ yarn add sass sass-loader
|
||||
$ yarn encore dev-server
|
||||
```
|
||||
|
||||
### 10. C'est installé !
|
||||
|
||||
le site est disponible sur http://localhost:8000
|
||||
Cette branche démarre d'une installation toute prête de la demo Symfony. La db est enregistrée dans le repo dans un simple fichier sqlite (`app/data/database.sqlite`). Le conteneur docker postgresql est donc désactivé.
|
||||
|
10
app/.editorconfig
Normal file
10
app/.editorconfig
Normal file
@ -0,0 +1,10 @@
|
||||
; top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
; Unix-style newlines
|
||||
[*]
|
||||
end_of_line = LF
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
indent_size = 4
|
16
app/.env
16
app/.env
@ -9,21 +9,25 @@
|
||||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
# https://symfony.com/doc/current/configuration/secrets.html
|
||||
#
|
||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=0751e183ea472eb11c19f21f66a2543c
|
||||
APP_SECRET=2ca64f8d83b9e89f5f19d672841d6bb8
|
||||
#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
#TRUSTED_HOSTS='^(localhost|example\.com)$'
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||
# For a MySQL database, use: "mysql://db_user:db_password@127.0.0.1:3306/db_name"
|
||||
# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
|
||||
# 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://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4"
|
||||
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
|
||||
DATABASE_URL=sqlite:///%kernel.project_dir%/data/database.sqlite
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
# MAILER_DSN=smtp://localhost
|
||||
###< symfony/mailer ###
|
||||
|
6
app/.env.test
Normal file
6
app/.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
|
||||
DATABASE_URL=sqlite:///%kernel.project_dir%/data/database_test.sqlite
|
80
app/.github/workflows/ci.yaml
vendored
Normal file
80
app/.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
name: "CI"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
env:
|
||||
fail-fast: true
|
||||
PHPUNIT_FLAGS: "-v"
|
||||
SYMFONY_PHPUNIT_DIR: "$HOME/symfony-bridge/.phpunit"
|
||||
SYMFONY_REQUIRE: ">=4.4"
|
||||
# 40x: Since symfony/monolog-bridge 5.2:
|
||||
# Passing an actionLevel (int|string) as constructor's 3rd argument of
|
||||
# "Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy"
|
||||
# is deprecated, "Monolog\Handler\FingersCrossed\ActivationStrategyInterface" expected.
|
||||
SYMFONY_DEPRECATIONS_HELPER: 40
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: "${{ matrix.operating-system }} / PHP ${{ matrix.php-version }}"
|
||||
runs-on: ${{ matrix.operating-system }}
|
||||
continue-on-error: false
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
operating-system: ['ubuntu-latest', 'windows-latest', 'macos-latest']
|
||||
php-version: ['7.2.9', '7.3', '7.4', '8.0']
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2.3.3
|
||||
|
||||
- name: "Install PHP with extensions"
|
||||
uses: shivammathur/setup-php@2.7.0
|
||||
with:
|
||||
coverage: "none"
|
||||
extensions: "intl, mbstring, pdo_sqlite"
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer:v2
|
||||
|
||||
- name: "Add PHPUnit matcher"
|
||||
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||
|
||||
- name: "Set composer cache directory"
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: "Cache composer"
|
||||
uses: actions/cache@v2.1.2
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }}
|
||||
restore-keys: ${{ runner.os }}-${{ matrix.php-version }}-composer-
|
||||
|
||||
- name: "Require symfony/flex"
|
||||
run: composer global require --no-progress --no-scripts --no-plugins symfony/flex dev-main
|
||||
|
||||
- if: matrix.php-version != '8.0'
|
||||
run: composer update
|
||||
|
||||
- if: matrix.php-version == '8.0'
|
||||
run: composer update --ignore-platform-req=php
|
||||
|
||||
- if: matrix.php-version != '8.0'
|
||||
name: "Install PHPUnit"
|
||||
run: vendor/bin/simple-phpunit install
|
||||
|
||||
- if: matrix.php-version == '8.0'
|
||||
name: "Install PHPUnit for PHP 8"
|
||||
run: |
|
||||
echo 'SYMFONY_PHPUNIT_VERSION=9.4' >> $GITHUB_ENV
|
||||
vendor/bin/simple-phpunit install
|
||||
|
||||
- name: "PHPUnit version"
|
||||
run: vendor/bin/simple-phpunit --version
|
||||
|
||||
- name: "Run tests"
|
||||
run: vendor/bin/simple-phpunit ${{ env.PHPUNIT_FLAGS }}
|
88
app/.github/workflows/lint.yaml
vendored
Normal file
88
app/.github/workflows/lint.yaml
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
name: "Lint"
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
fail-fast: true
|
||||
|
||||
jobs:
|
||||
php-cs-fixer:
|
||||
name: PHP-CS-Fixer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: PHP-CS-Fixer
|
||||
uses: docker://oskarstark/php-cs-fixer-ga
|
||||
with:
|
||||
args: --diff --dry-run
|
||||
|
||||
linters:
|
||||
name: Linters
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['7.4']
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2.3.3
|
||||
|
||||
- name: "Install PHP with extensions"
|
||||
uses: shivammathur/setup-php@2.7.0
|
||||
with:
|
||||
coverage: "none"
|
||||
extensions: intl
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer:v2
|
||||
|
||||
- name: "Set composer cache directory"
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: "Cache composer"
|
||||
uses: actions/cache@v2.1.2
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }}
|
||||
restore-keys: ${{ runner.os }}-${{ matrix.php-version }}-composer-
|
||||
|
||||
- name: "Require symfony/flex"
|
||||
run: composer global require --no-progress --no-scripts --no-plugins symfony/flex dev-main
|
||||
|
||||
- name: "Composer update"
|
||||
id: install
|
||||
run: composer update --no-scripts
|
||||
|
||||
- name: Lint YAML files
|
||||
if: always() && steps.install.outcome == 'success'
|
||||
run: ./bin/console lint:yaml config --parse-tags
|
||||
|
||||
- name: Lint Twig templates
|
||||
if: always() && steps.install.outcome == 'success'
|
||||
run: ./bin/console lint:twig templates --env=prod
|
||||
|
||||
- name: Lint XLIFF translations
|
||||
if: always() && steps.install.outcome == 'success'
|
||||
run: ./bin/console lint:xliff translations
|
||||
|
||||
- name: Lint Parameters and Services
|
||||
if: always() && steps.install.outcome == 'success'
|
||||
run: ./bin/console lint:container
|
||||
|
||||
- name: Lint Doctrine entities
|
||||
if: always() && steps.install.outcome == 'success'
|
||||
run: ./bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction
|
||||
|
||||
- name: Lint Composer config
|
||||
if: always() && steps.install.outcome == 'success'
|
||||
run: composer validate --strict
|
||||
|
||||
- name: Download Symfony CLI
|
||||
if: always() && steps.install.outcome == 'success'
|
||||
run: wget https://get.symfony.com/cli/installer -O - | bash
|
||||
|
||||
- name: Check if any dependencies are compromised
|
||||
if: always() && steps.install.outcome == 'success'
|
||||
run: /home/runner/.symfony/bin/symfony check:security
|
7
app/.gitignore
vendored
7
app/.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
/public/build/fonts/glyphicons-*
|
||||
/public/build/images/glyphicons-*
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
@ -9,6 +11,11 @@
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit
|
||||
.phpunit.result.cache
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
###> symfony/webpack-encore-bundle ###
|
||||
/node_modules/
|
||||
/public/build/
|
||||
|
42
app/.php_cs.dist
Normal file
42
app/.php_cs.dist
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
$fileHeaderComment = <<<COMMENT
|
||||
This file is part of the Symfony package.
|
||||
|
||||
(c) Fabien Potencier <fabien@symfony.com>
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
COMMENT;
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->in(__DIR__)
|
||||
->exclude('config')
|
||||
->exclude('var')
|
||||
->exclude('public/bundles')
|
||||
->exclude('public/build')
|
||||
// exclude files generated by Symfony Flex recipes
|
||||
->notPath('bin/console')
|
||||
->notPath('public/index.php')
|
||||
;
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
'@Symfony:risky' => true,
|
||||
'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'],
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'mb_str_functions' => true,
|
||||
'no_php4_constructor' => true,
|
||||
'no_unreachable_default_argument_value' => true,
|
||||
'no_useless_else' => true,
|
||||
'no_useless_return' => true,
|
||||
'php_unit_strict' => true,
|
||||
'phpdoc_order' => true,
|
||||
'strict_comparison' => true,
|
||||
'strict_param' => true,
|
||||
])
|
||||
->setFinder($finder)
|
||||
->setCacheFile(__DIR__.'/var/.php_cs.cache')
|
||||
;
|
7
app/CONTRIBUTING.md
Normal file
7
app/CONTRIBUTING.md
Normal file
@ -0,0 +1,7 @@
|
||||
Contributing
|
||||
============
|
||||
|
||||
The Symfony Demo application is an open source project. Contributions made by
|
||||
the community are welcome. Send us your ideas, code reviews, pull requests and
|
||||
feature requests to help us improve this project. All contributions must follow
|
||||
the [usual Symfony contribution requirements](https://symfony.com/doc/current/contributing/index.html).
|
19
app/LICENSE
Normal file
19
app/LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2015-2020 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
60
app/README.md
Normal file
60
app/README.md
Normal file
@ -0,0 +1,60 @@
|
||||
Symfony Demo Application
|
||||
========================
|
||||
|
||||
The "Symfony Demo Application" is a reference application created to show how
|
||||
to develop applications following the [Symfony Best Practices][1].
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
* PHP 7.2.9 or higher;
|
||||
* PDO-SQLite PHP extension enabled;
|
||||
* and the [usual Symfony application requirements][2].
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
[Download Symfony][4] to install the `symfony` binary on your computer and run
|
||||
this command:
|
||||
|
||||
```bash
|
||||
$ symfony new --demo my_project
|
||||
```
|
||||
|
||||
Alternatively, you can use Composer:
|
||||
|
||||
```bash
|
||||
$ composer create-project symfony/symfony-demo my_project
|
||||
```
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
There's no need to configure anything to run the application. If you have
|
||||
[installed Symfony][4] binary, run this command:
|
||||
|
||||
```bash
|
||||
$ cd my_project/
|
||||
$ symfony serve
|
||||
```
|
||||
|
||||
Then access the application in your browser at the given URL (<https://localhost:8000> by default).
|
||||
|
||||
If you don't have the Symfony binary installed, run `php -S localhost:8000 -t public/`
|
||||
to use the built-in PHP web server or [configure a web server][3] like Nginx or
|
||||
Apache to run the application.
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
Execute this command to run tests:
|
||||
|
||||
```bash
|
||||
$ cd my_project/
|
||||
$ ./bin/phpunit
|
||||
```
|
||||
|
||||
[1]: https://symfony.com/doc/current/best_practices.html
|
||||
[2]: https://symfony.com/doc/current/reference/requirements.html
|
||||
[3]: https://symfony.com/doc/current/cookbook/configuration/web_server_configuration.html
|
||||
[4]: https://symfony.com/download
|
@ -1,12 +0,0 @@
|
||||
/*
|
||||
* Welcome to your app's main JavaScript file!
|
||||
*
|
||||
* We recommend including the built version of this JavaScript file
|
||||
* (and its CSS file) in your base layout (base.html.twig).
|
||||
*/
|
||||
|
||||
// any CSS you import will output into a single css file (app.css in this case)
|
||||
import './styles/app.css';
|
||||
|
||||
// start the Stimulus application
|
||||
import './bootstrap';
|
11
app/assets/bootstrap.js
vendored
11
app/assets/bootstrap.js
vendored
@ -1,11 +0,0 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bridge';
|
||||
|
||||
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
|
||||
export const app = startStimulusApp(require.context(
|
||||
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
|
||||
true,
|
||||
/\.[jt]sx?$/
|
||||
));
|
||||
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"controllers": [],
|
||||
"entrypoints": []
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* This is an example Stimulus controller!
|
||||
*
|
||||
* Any element with a data-controller="hello" attribute will cause
|
||||
* this controller to be executed. The name "hello" comes from the filename:
|
||||
* hello_controller.js -> "hello"
|
||||
*
|
||||
* Delete this file or adapt it for your use!
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
|
||||
}
|
||||
}
|
64
app/assets/js/admin.js
Normal file
64
app/assets/js/admin.js
Normal file
@ -0,0 +1,64 @@
|
||||
import '../scss/admin.scss';
|
||||
import 'eonasdan-bootstrap-datetimepicker';
|
||||
import 'typeahead.js';
|
||||
import Bloodhound from "bloodhound-js";
|
||||
import 'bootstrap-tagsinput';
|
||||
|
||||
$(function() {
|
||||
// Datetime picker initialization.
|
||||
// See https://eonasdan.github.io/bootstrap-datetimepicker/
|
||||
$('[data-toggle="datetimepicker"]').datetimepicker({
|
||||
icons: {
|
||||
time: 'fa fa-clock-o',
|
||||
date: 'fa fa-calendar',
|
||||
up: 'fa fa-chevron-up',
|
||||
down: 'fa fa-chevron-down',
|
||||
previous: 'fa fa-chevron-left',
|
||||
next: 'fa fa-chevron-right',
|
||||
today: 'fa fa-check-circle-o',
|
||||
clear: 'fa fa-trash',
|
||||
close: 'fa fa-remove'
|
||||
}
|
||||
});
|
||||
|
||||
// Bootstrap-tagsinput initialization
|
||||
// https://bootstrap-tagsinput.github.io/bootstrap-tagsinput/examples/
|
||||
var $input = $('input[data-toggle="tagsinput"]');
|
||||
if ($input.length) {
|
||||
var source = new Bloodhound({
|
||||
local: $input.data('tags'),
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||
datumTokenizer: Bloodhound.tokenizers.whitespace
|
||||
});
|
||||
source.initialize();
|
||||
|
||||
$input.tagsinput({
|
||||
trimValue: true,
|
||||
focusClass: 'focus',
|
||||
typeaheadjs: {
|
||||
name: 'tags',
|
||||
source: source.ttAdapter()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handling the modal confirmation message.
|
||||
$(document).on('submit', 'form[data-confirmation]', function (event) {
|
||||
var $form = $(this),
|
||||
$confirm = $('#confirmationModal');
|
||||
|
||||
if ($confirm.data('result') !== 'yes') {
|
||||
//cancel submit event
|
||||
event.preventDefault();
|
||||
|
||||
$confirm
|
||||
.off('click', '#btnYes')
|
||||
.on('click', '#btnYes', function () {
|
||||
$confirm.data('result', 'yes');
|
||||
$form.find('input[type="submit"]').attr('disabled', 'disabled');
|
||||
$form.submit();
|
||||
})
|
||||
.modal('show');
|
||||
}
|
||||
});
|
15
app/assets/js/app.js
Normal file
15
app/assets/js/app.js
Normal file
@ -0,0 +1,15 @@
|
||||
import '../scss/app.scss';
|
||||
|
||||
// loads the Bootstrap jQuery plugins
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/transition.js';
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/alert.js';
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/collapse.js';
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown.js';
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/modal.js';
|
||||
import 'jquery'
|
||||
|
||||
// loads the code syntax highlighting library
|
||||
import './highlight.js';
|
||||
|
||||
// Creates links to the Symfony documentation
|
||||
import './doclinks.js';
|
58
app/assets/js/doclinks.js
Normal file
58
app/assets/js/doclinks.js
Normal file
@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
// Wraps some elements in anchor tags referencing to the Symfony documentation
|
||||
$(function() {
|
||||
var $modal = $('#sourceCodeModal');
|
||||
var $controllerCode = $modal.find('code.php');
|
||||
var $templateCode = $modal.find('code.twig');
|
||||
|
||||
function anchor(url, content) {
|
||||
return '<a class="doclink" target="_blank" href="' + url + '">' + content + '</a>';
|
||||
};
|
||||
|
||||
// Wraps links to the Symfony documentation
|
||||
$modal.find('.hljs-comment').each(function() {
|
||||
$(this).html($(this).html().replace(/https:\/\/symfony.com\/doc\/[\w/.#-]+/g, function(url) {
|
||||
return anchor(url, url);
|
||||
}));
|
||||
});
|
||||
|
||||
// Wraps Symfony's annotations
|
||||
var annotations = {
|
||||
'@Cache': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/cache.html',
|
||||
'@IsGranted': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html#isgranted',
|
||||
'@ParamConverter': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html',
|
||||
'@Route': 'https://symfony.com/doc/current/routing.html#creating-routes-as-annotations',
|
||||
'@Security': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html#security'
|
||||
};
|
||||
|
||||
$controllerCode.find('.hljs-doctag').each(function() {
|
||||
var annotation = $(this).text();
|
||||
|
||||
if (annotations[annotation]) {
|
||||
$(this).html(anchor(annotations[annotation], annotation));
|
||||
}
|
||||
});
|
||||
|
||||
// Wraps Twig's tags
|
||||
$templateCode.find('.hljs-template-tag > .hljs-name').each(function() {
|
||||
var tag = $(this).text();
|
||||
|
||||
if ('else' === tag || tag.match(/^end/)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = 'https://twig.symfony.com/doc/3.x/tags/' + tag + '.html#' + tag;
|
||||
|
||||
$(this).html(anchor(url, tag));
|
||||
});
|
||||
|
||||
// Wraps Twig's functions
|
||||
$templateCode.find('.hljs-template-variable > .hljs-name').each(function() {
|
||||
var func = $(this).text();
|
||||
|
||||
var url = 'https://twig.symfony.com/doc/3.x/functions/' + func + '.html#' + func;
|
||||
|
||||
$(this).html(anchor(url, func));
|
||||
});
|
||||
});
|
8
app/assets/js/highlight.js
Normal file
8
app/assets/js/highlight.js
Normal file
@ -0,0 +1,8 @@
|
||||
import hljs from 'highlight.js/lib/highlight';
|
||||
import php from 'highlight.js/lib/languages/php';
|
||||
import twig from 'highlight.js/lib/languages/twig';
|
||||
|
||||
hljs.registerLanguage('php', php);
|
||||
hljs.registerLanguage('twig', twig);
|
||||
|
||||
hljs.initHighlightingOnLoad();
|
106
app/assets/js/jquery.instantSearch.js
Normal file
106
app/assets/js/jquery.instantSearch.js
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* jQuery plugin for an instant searching.
|
||||
*
|
||||
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
String.prototype.render = function (parameters) {
|
||||
return this.replace(/({{ (\w+) }})/g, function (match, pattern, name) {
|
||||
return parameters[name];
|
||||
})
|
||||
};
|
||||
|
||||
// INSTANTS SEARCH PUBLIC CLASS DEFINITION
|
||||
// =======================================
|
||||
|
||||
var InstantSearch = function (element, options) {
|
||||
this.$input = $(element);
|
||||
this.$form = this.$input.closest('form');
|
||||
this.$preview = $('<ul class="search-preview list-group">').appendTo(this.$form);
|
||||
this.options = $.extend({}, InstantSearch.DEFAULTS, this.$input.data(), options);
|
||||
|
||||
this.$input.keyup(this.debounce());
|
||||
};
|
||||
|
||||
InstantSearch.DEFAULTS = {
|
||||
minQueryLength: 2,
|
||||
limit: 10,
|
||||
delay: 500,
|
||||
noResultsMessage: 'No results found',
|
||||
itemTemplate: '\
|
||||
<article class="post">\
|
||||
<h2><a href="{{ url }}">{{ title }}</a></h2>\
|
||||
<p class="post-metadata">\
|
||||
<span class="metadata"><i class="fa fa-calendar"></i> {{ date }}</span>\
|
||||
<span class="metadata"><i class="fa fa-user"></i> {{ author }}</span>\
|
||||
</p>\
|
||||
<p>{{ summary }}</p>\
|
||||
</article>'
|
||||
};
|
||||
|
||||
InstantSearch.prototype.debounce = function () {
|
||||
var delay = this.options.delay;
|
||||
var search = this.search;
|
||||
var timer = null;
|
||||
var self = this;
|
||||
|
||||
return function () {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
search.apply(self);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
InstantSearch.prototype.search = function () {
|
||||
var query = $.trim(this.$input.val()).replace(/\s{2,}/g, ' ');
|
||||
if (query.length < this.options.minQueryLength) {
|
||||
this.$preview.empty();
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var data = this.$form.serializeArray();
|
||||
data['l'] = this.limit;
|
||||
|
||||
$.getJSON(this.$form.attr('action'), data, function (items) {
|
||||
self.show(items);
|
||||
});
|
||||
};
|
||||
|
||||
InstantSearch.prototype.show = function (items) {
|
||||
var $preview = this.$preview;
|
||||
var itemTemplate = this.options.itemTemplate;
|
||||
|
||||
if (0 === items.length) {
|
||||
$preview.html(this.options.noResultsMessage);
|
||||
} else {
|
||||
$preview.empty();
|
||||
$.each(items, function (index, item) {
|
||||
$preview.append(itemTemplate.render(item));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// INSTANTS SEARCH PLUGIN DEFINITION
|
||||
// =================================
|
||||
|
||||
function Plugin(option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this);
|
||||
var instance = $this.data('instantSearch');
|
||||
var options = typeof option === 'object' && option;
|
||||
|
||||
if (!instance) $this.data('instantSearch', (instance = new InstantSearch(this, options)));
|
||||
|
||||
if (option === 'search') instance.search();
|
||||
})
|
||||
}
|
||||
|
||||
$.fn.instantSearch = Plugin;
|
||||
$.fn.instantSearch.Constructor = InstantSearch;
|
||||
|
||||
})(window.jQuery);
|
11
app/assets/js/login.js
Normal file
11
app/assets/js/login.js
Normal file
@ -0,0 +1,11 @@
|
||||
$(function() {
|
||||
var usernameEl = $('#username');
|
||||
var passwordEl = $('#password');
|
||||
|
||||
// in a real application, the user/password should never be hardcoded
|
||||
// but for the demo application it's very convenient to do so
|
||||
if (!usernameEl.val() || 'jane_admin' === usernameEl.val()) {
|
||||
usernameEl.val('jane_admin');
|
||||
passwordEl.val('kitten');
|
||||
}
|
||||
});
|
9
app/assets/js/search.js
Normal file
9
app/assets/js/search.js
Normal file
@ -0,0 +1,9 @@
|
||||
import './jquery.instantSearch.js';
|
||||
|
||||
$(function() {
|
||||
$('.search-field')
|
||||
.instantSearch({
|
||||
delay: 100,
|
||||
})
|
||||
.keyup();
|
||||
});
|
26
app/assets/scss/admin.scss
Normal file
26
app/assets/scss/admin.scss
Normal file
@ -0,0 +1,26 @@
|
||||
@import "~bootswatch/flatly/variables";
|
||||
@import "~eonasdan-bootstrap-datetimepicker/src/sass/bootstrap-datetimepicker-build.scss";
|
||||
@import "bootstrap-tagsinput.scss";
|
||||
|
||||
/* Page: 'Backend post index'
|
||||
------------------------------------------------------------------------- */
|
||||
body#admin_post_index .item-actions {
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
body#admin_post_index .item-actions a.btn + a.btn {
|
||||
margin-left: 4px
|
||||
}
|
||||
|
||||
/* Page: 'Backend post show'
|
||||
------------------------------------------------------------------------- */
|
||||
body#admin_post_show .post-tags .label-default {
|
||||
background-color: #e9ecec;
|
||||
color: #6D8283;
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
padding: .4em 1em .5em;
|
||||
}
|
||||
body#admin_post_show .post-tags .label-default i {
|
||||
color: #95A6A7;
|
||||
}
|
360
app/assets/scss/app.scss
Normal file
360
app/assets/scss/app.scss
Normal file
@ -0,0 +1,360 @@
|
||||
// setting the value of this variable to an empty data URL is the only working solution
|
||||
// to load the Bootswatch web fonts locally and avoid loading them from Google servers
|
||||
// see https://github.com/thomaspark/bootswatch/issues/55#issuecomment-298093182
|
||||
$web-font-path: 'data:text/css;base64,';
|
||||
|
||||
// Make sure the bootstrap-sass and lato fonts are resolved correctly
|
||||
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
|
||||
$lato-font-path: '~lato-font/fonts';
|
||||
|
||||
@import "~bootswatch/flatly/variables";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap";
|
||||
@import "~bootswatch/flatly/bootswatch";
|
||||
@import "~@fortawesome/fontawesome-free/css/all.css";
|
||||
@import "~@fortawesome/fontawesome-free/css/v4-shims.css";
|
||||
@import "~highlight.js/styles/solarized-light.css";
|
||||
|
||||
// pick the Lato fonts individually to avoid importing the entire font family
|
||||
@import '~lato-font/scss/public-api';
|
||||
@include lato-include-font('normal');
|
||||
@include lato-include-font('bold');
|
||||
|
||||
/* Basic styles
|
||||
------------------------------------------------------------------------- */
|
||||
p, ul, ol {
|
||||
font-size: 19px;
|
||||
margin-bottom: 1.5em
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em
|
||||
}
|
||||
|
||||
code {
|
||||
background: #ecf0f1;
|
||||
color: #2c3e50
|
||||
}
|
||||
|
||||
.text-danger, .text-danger:hover {
|
||||
color: #e74c3c
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 0.25em
|
||||
}
|
||||
|
||||
.table.table-middle-aligned th,
|
||||
.table.table-middle-aligned td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.doclink {
|
||||
color: inherit
|
||||
}
|
||||
|
||||
/* Utilities
|
||||
------------------------------------------------------------------------- */
|
||||
.m-b-0 { margin-bottom: 0 }
|
||||
|
||||
/* Page elements
|
||||
------------------------------------------------------------------------- */
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 2em
|
||||
}
|
||||
|
||||
header ul.nav li {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
header .locales {
|
||||
min-width: 190px;
|
||||
}
|
||||
header .locales a {
|
||||
color: #212529;
|
||||
padding: 3px 15px;
|
||||
}
|
||||
header .locales a small {
|
||||
border-radius: 4px;
|
||||
border: 2px solid #dee2e6;
|
||||
color: #7b8a8b;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
line-height: 1.1;
|
||||
margin: 2px 10px 0 0;
|
||||
min-width: 26px;
|
||||
padding: 0px 3px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
header .locales .active small,
|
||||
header .locales a:hover small {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.body-container {
|
||||
flex: 1;
|
||||
/* needed to prevent pages with a very small height and browsers not supporting flex */
|
||||
min-height: 600px
|
||||
}
|
||||
|
||||
.body-container #main h1, .body-container #main h2 {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
#sidebar .section {
|
||||
margin-bottom: 2em
|
||||
}
|
||||
|
||||
#sidebar p {
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
#sidebar p + p {
|
||||
margin: 1.5em 0 0
|
||||
}
|
||||
|
||||
footer {
|
||||
background: #ecf0f1;
|
||||
margin-top: 2em;
|
||||
padding-top: 2em;
|
||||
padding-bottom: 2em
|
||||
}
|
||||
|
||||
footer p {
|
||||
color: #7b8a8b;
|
||||
font-size: 13px;
|
||||
margin-bottom: 0.25em
|
||||
}
|
||||
|
||||
footer #footer-resources {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
footer #footer-resources i {
|
||||
color: #7b8a8b;
|
||||
font-size: 28.5px;
|
||||
margin-left: 0.5em
|
||||
}
|
||||
|
||||
#sourceCodeModal h3 {
|
||||
font-size: 19px;
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
#sourceCodeModal h3 small {
|
||||
color: #7b8a8b;
|
||||
font-size: 80%
|
||||
}
|
||||
|
||||
#sourceCodeModal pre {
|
||||
margin-bottom: 2em;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
#confirmationModal .modal-dialog {
|
||||
width: 500px
|
||||
}
|
||||
|
||||
#confirmationModal .modal-footer button {
|
||||
min-width: 75px
|
||||
}
|
||||
|
||||
/* Misc. elements
|
||||
------------------------------------------------------------------------- */
|
||||
.section.rss a {
|
||||
color: #f39c12;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
------------------------------------------------------------------------- */
|
||||
.form-group.has-error .form-control {
|
||||
border-color: #e74c3c
|
||||
}
|
||||
|
||||
.form-group.has-error .control-label {
|
||||
color: #e74c3c
|
||||
}
|
||||
|
||||
.form-group.has-error .help-block {
|
||||
background-color: #e74c3c;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
padding: 1em
|
||||
}
|
||||
|
||||
.form-group.has-error .help-block ul,
|
||||
.form-group.has-error .help-block li {
|
||||
margin-bottom: 0
|
||||
}
|
||||
.form-group.has-error .help-block li + li {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: 100%
|
||||
}
|
||||
|
||||
/* Page: 'Technical Requirements Checker'
|
||||
------------------------------------------------------------------------- */
|
||||
body#requirements_checker header h1 {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
body#requirements_checker header h1 span {
|
||||
font-size: 120%;
|
||||
opacity: 0.7;
|
||||
padding: 0 5px
|
||||
}
|
||||
|
||||
body#requirements_checker .panel li {
|
||||
margin-bottom: 1em
|
||||
}
|
||||
|
||||
/* Page: 'Homepage'
|
||||
------------------------------------------------------------------------- */
|
||||
body#homepage {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
/* Page: 'Login'
|
||||
------------------------------------------------------------------------- */
|
||||
body#login #login-users-help p {
|
||||
font-size: 15px;
|
||||
line-height: 1.42857
|
||||
}
|
||||
|
||||
body#login #login-users-help p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
body#login #login-users-help p .label {
|
||||
margin-right: 5px
|
||||
}
|
||||
|
||||
body#login #login-users-help p .console {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
/* Common Blog page elements
|
||||
------------------------------------------------------------------------- */
|
||||
.post-metadata {
|
||||
color: #b4bcc2;
|
||||
font-size: 19px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.post-metadata .metadata {
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
.post-tags .label {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Page: 'Blog index'
|
||||
------------------------------------------------------------------------- */
|
||||
body#blog_index #main h1,
|
||||
body#blog_index #main p {
|
||||
margin-bottom: 0.5em
|
||||
}
|
||||
|
||||
body#blog_index article.post {
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
body#blog_index .post-metadata {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
body#blog_index .post-tags .label-default {
|
||||
background-color: #e9ecec;
|
||||
color: #6d8283;
|
||||
}
|
||||
body#blog_index .post-tags .label-default i {
|
||||
color: #a3b2b2;
|
||||
}
|
||||
|
||||
/* Page: 'Blog post show'
|
||||
------------------------------------------------------------------------- */
|
||||
body#blog_post_show #main h3 {
|
||||
margin-bottom: 0.75em
|
||||
}
|
||||
|
||||
body#blog_post_show .post-tags .label-default {
|
||||
background-color: #e9ecec;
|
||||
color: #6D8283;
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
padding: .4em 1em .5em;
|
||||
}
|
||||
body#blog_post_show .post-tags .label-default i {
|
||||
color: #95A6A7;
|
||||
}
|
||||
|
||||
body#blog_post_show #post-add-comment {
|
||||
margin: 2em 0
|
||||
}
|
||||
|
||||
body#blog_post_show #post-add-comment p {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
body#blog_post_show #post-add-comment p a.btn {
|
||||
margin-right: 0.5em
|
||||
}
|
||||
|
||||
body#blog_post_show .post-comment {
|
||||
margin-bottom: 2em
|
||||
}
|
||||
|
||||
body#blog_post_show .post-comment h4 {
|
||||
font-size: 13px;
|
||||
line-height: 1.42857;
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
body#blog_post_show .post-comment h4 strong {
|
||||
display: block
|
||||
}
|
||||
|
||||
/* Page: 'Comment form error'
|
||||
------------------------------------------------------------------------- */
|
||||
body#comment_form_error h1.text-danger {
|
||||
margin-bottom: 1em
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 1200px) {
|
||||
.container {
|
||||
width: 98%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Page: 'Blog search'
|
||||
------------------------------------------------------------------------- */
|
||||
body#blog_search #main h1,
|
||||
body#blog_search #main p {
|
||||
margin-bottom: 0.5em
|
||||
}
|
||||
|
||||
body#blog_search article.post:first-child {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
body#blog_search article.post {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
body#blog_search .post-metadata {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
194
app/assets/scss/bootstrap-tagsinput.scss
vendored
Normal file
194
app/assets/scss/bootstrap-tagsinput.scss
vendored
Normal file
@ -0,0 +1,194 @@
|
||||
/* ------------------------------------------------------------------------------
|
||||
*
|
||||
* # Twiter Typeahead
|
||||
*
|
||||
* Styles for tagsinput.js - input suggestion engine
|
||||
*
|
||||
* ---------------------------------------------------------------------------- */
|
||||
.twitter-typeahead {
|
||||
width: 100%;
|
||||
}
|
||||
.typeahead,
|
||||
.tt-query,
|
||||
.tt-hint {
|
||||
outline: 0;
|
||||
}
|
||||
.tt-hint {
|
||||
color: #999;
|
||||
}
|
||||
.tt-menu{
|
||||
width: 100%;
|
||||
margin-top: 1px;
|
||||
min-width: 180px;
|
||||
padding: 7px 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0,0,0,0.15);
|
||||
border-radius: 4px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,0.175);
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.175);
|
||||
-webkit-background-clip: padding-box;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.typeahead-scrollable .tt-menu{
|
||||
max-height: 250px;
|
||||
}
|
||||
.typeahead-rtl .tt-menu{
|
||||
text-align: right;
|
||||
}
|
||||
.tt-suggestion {
|
||||
padding: 8px 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tt-suggestion.tt-cursor {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.tt-suggestion p {
|
||||
margin: 0;
|
||||
}
|
||||
.tt-suggestion.tt-selectable:before {
|
||||
content: '\f02b';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
margin-right: 0.5em;
|
||||
color: inherit;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.tt-dataset-group .tt-suggestion {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
.tt-heading {
|
||||
font-size: 11px;
|
||||
line-height: 1.82;
|
||||
padding: 8px 15px;
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.tt-suggestion:hover,
|
||||
.tt-suggestion:focus {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
background-color: #18bc9c;
|
||||
}
|
||||
/* ------------------------------------------------------------------------------
|
||||
*
|
||||
* # Bootstrap tags input
|
||||
*
|
||||
* Styles for tagsinput.js - tags input for Bootstrap
|
||||
*
|
||||
* ---------------------------------------------------------------------------- */
|
||||
.bootstrap-tagsinput {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
padding: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.42857143;
|
||||
color: #2c3e50;
|
||||
background-color: #ffffff;
|
||||
background-image: none;
|
||||
border: 2px solid #dce4ec;
|
||||
border-radius: 4px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
}
|
||||
.has-error .bootstrap-tagsinput {
|
||||
border-color: #e74c3c !important;
|
||||
}
|
||||
.bootstrap-tagsinput.focus {
|
||||
border-color: #2c3e50;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.bootstrap-tagsinput input {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background-color: transparent;
|
||||
padding: 5px 11px;
|
||||
margin-top: 2px;
|
||||
margin-left: 2px;
|
||||
width: auto !important;
|
||||
min-width: 100px;
|
||||
font-size: 15px;
|
||||
line-height: 1.6666667;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.bootstrap-tagsinput input:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.bootstrap-tagsinput .twitter-typeahead {
|
||||
width: auto;
|
||||
}
|
||||
.bootstrap-tagsinput .tt-menu {
|
||||
margin-top: 5px;
|
||||
min-width: 200px;
|
||||
}
|
||||
.bootstrap-tagsinput .tag {
|
||||
margin: 1px 0 0 3px;
|
||||
border: 0;
|
||||
border-radius: .25em;
|
||||
padding: 5px 11px;
|
||||
padding-right: 30px;
|
||||
float: left;
|
||||
font-size: 15px;
|
||||
line-height: 1.6666667;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
position: relative;
|
||||
background-color: #18bc9c;
|
||||
color: #fff;
|
||||
}
|
||||
.has-error .bootstrap-tagsinput .tag {
|
||||
background-color: #e74c3c !important;
|
||||
}
|
||||
.bootstrap-tagsinput .tag [data-role="remove"] {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 11px;
|
||||
line-height: 1;
|
||||
margin-top: -5.5px;
|
||||
opacity: 0.7;
|
||||
filter: alpha(opacity=70);
|
||||
}
|
||||
.bootstrap-tagsinput .tag [data-role="remove"]:hover {
|
||||
opacity: 1;
|
||||
filter: alpha(opacity=100);
|
||||
}
|
||||
.bootstrap-tagsinput .tag:before {
|
||||
content: '\f02b';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
margin-right: 0.5em;
|
||||
color: #fff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.bootstrap-tagsinput .tag [data-role="remove"]:after {
|
||||
content: '\f00d';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
body {
|
||||
background-color: lightgray;
|
||||
}
|
@ -3,15 +3,41 @@
|
||||
|
||||
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 (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
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;
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
set_time_limit(0);
|
||||
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
return new Application($kernel);
|
||||
};
|
||||
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);
|
||||
|
13
app/bin/phpunit
Executable file
13
app/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';
|
@ -1,46 +1,61 @@
|
||||
{
|
||||
"name": "symfony/symfony-demo",
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"description": "Symfony Demo Application",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"replace": {
|
||||
"symfony/polyfill-php70": "*",
|
||||
"symfony/polyfill-php72": "*"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"doctrine/annotations": "^1.13",
|
||||
"doctrine/doctrine-bundle": "^2.7",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.2",
|
||||
"doctrine/orm": "^2.13",
|
||||
"egulias/email-validator": "^3.2",
|
||||
"sensio/framework-extra-bundle": "^6.2",
|
||||
"symfony/asset": "^5.4",
|
||||
"symfony/console": "5.4.*",
|
||||
"symfony/dotenv": "5.4.*",
|
||||
"symfony/expression-language": "^5.4",
|
||||
"symfony/flex": "^1.17|^2",
|
||||
"symfony/form": "^5.4",
|
||||
"symfony/framework-bundle": "5.4.*",
|
||||
"symfony/intl": "^5.4",
|
||||
"symfony/maker-bundle": "^1.43",
|
||||
"symfony/proxy-manager-bridge": "5.4.*",
|
||||
"symfony/runtime": "5.4.*",
|
||||
"symfony/security-csrf": "^5.4",
|
||||
"symfony/security-http": "^5.4",
|
||||
"symfony/translation": "^5.4",
|
||||
"symfony/twig-bridge": "^5.4",
|
||||
"symfony/validator": "^5.4",
|
||||
"symfony/web-link": "^5.4",
|
||||
"symfony/webpack-encore-bundle": "^1.15",
|
||||
"symfony/yaml": "^5.4",
|
||||
"twig/twig": "^3.4"
|
||||
"php": "^7.2.9",
|
||||
"ext-pdo_sqlite": "*",
|
||||
"composer/package-versions-deprecated": "^1.8",
|
||||
"doctrine/doctrine-bundle": "^1.12|^2.0",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.5.11",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"sensio/framework-extra-bundle": "^5.6",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "^5.2",
|
||||
"symfony/console": "^5.2",
|
||||
"symfony/dotenv": "^5.2",
|
||||
"symfony/expression-language": "^5.2",
|
||||
"symfony/flex": "^1.1",
|
||||
"symfony/form": "^5.2",
|
||||
"symfony/framework-bundle": "^5.2",
|
||||
"symfony/intl": "^5.2",
|
||||
"symfony/mailer": "^5.2",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/polyfill-intl-messageformatter": "^1.12",
|
||||
"symfony/security-bundle": "^5.2",
|
||||
"symfony/string": "^5.2",
|
||||
"symfony/translation": "^5.2",
|
||||
"symfony/twig-pack": "^1.0",
|
||||
"symfony/validator": "^5.2",
|
||||
"symfony/webpack-encore-bundle": "^1.4",
|
||||
"symfony/yaml": "^5.2",
|
||||
"tgalopin/html-sanitizer-bundle": "^1.2",
|
||||
"twig/intl-extra": "^3.0",
|
||||
"twig/markdown-extra": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^6.2",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.0",
|
||||
"symfony/browser-kit": "^5.2",
|
||||
"symfony/css-selector": "^5.2",
|
||||
"symfony/debug-bundle": "^5.2",
|
||||
"symfony/maker-bundle": "^1.11",
|
||||
"symfony/phpunit-bridge": "^5.2",
|
||||
"symfony/stopwatch": "^5.2",
|
||||
"symfony/web-profiler-bundle": "^5.2"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
"platform": {
|
||||
"php": "7.2.9"
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
},
|
||||
@ -56,15 +71,10 @@
|
||||
"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"
|
||||
"assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
@ -78,14 +88,7 @@
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "5.4.*"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/debug-bundle": "^5.4",
|
||||
"symfony/stopwatch": "5.4.*",
|
||||
"symfony/var-dumper": "^5.4",
|
||||
"symfony/web-profiler-bundle": "5.4.*"
|
||||
"allow-contrib": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4782
app/composer.lock
generated
4782
app/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -2,12 +2,18 @@
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
HtmlSanitizer\Bundle\HtmlSanitizerBundle::class => ['all' => true],
|
||||
];
|
||||
|
3
app/config/packages/assets.yaml
Normal file
3
app/config/packages/assets.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
@ -1,5 +0,0 @@
|
||||
when@dev:
|
||||
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)%"
|
4
app/config/packages/dev/debug.yaml
Normal file
4
app/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)%"
|
4
app/config/packages/dev/mailer.yaml
Normal file
4
app/config/packages/dev/mailer.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
framework:
|
||||
mailer:
|
||||
# this disables delivery of messages entirely
|
||||
dsn: 'null://null'
|
19
app/config/packages/dev/monolog.yaml
Normal file
19
app/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
app/config/packages/dev/web_profiler.yaml
Normal file
6
app/config/packages/dev/web_profiler.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
@ -4,7 +4,7 @@ doctrine:
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
#server_version: '15'
|
||||
#server_version: '5.7'
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
@ -12,32 +12,7 @@ doctrine:
|
||||
mappings:
|
||||
App:
|
||||
is_bundle: false
|
||||
type: annotation
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
dbal:
|
||||
# "TEST_TOKEN" is typically set by ParaTest
|
||||
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
auto_generate_proxy_classes: false
|
||||
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
|
@ -3,4 +3,3 @@ doctrine_migrations:
|
||||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
||||
|
@ -1,8 +1,8 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
#csrf_protection: true
|
||||
http_method_override: false
|
||||
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.
|
||||
@ -10,15 +10,18 @@ framework:
|
||||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
# When using the HTTP Cache, ESI allows to render page fragments separately
|
||||
# and with different cache configurations for each fragment
|
||||
# https://symfony.com/doc/current/http_cache/esi.html
|
||||
esi: true
|
||||
fragments: true
|
||||
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
# The 'ide' option turns all of the file paths in an exception page
|
||||
# into clickable links that open the given file using your favorite IDE.
|
||||
# When 'ide' is set to null the file is opened in your web browser.
|
||||
# See https://symfony.com/doc/current/reference/configuration/framework.html#ide
|
||||
ide: null
|
||||
|
17
app/config/packages/html_sanitizer.yaml
Normal file
17
app/config/packages/html_sanitizer.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
html_sanitizer:
|
||||
default_sanitizer: 'default'
|
||||
sanitizers:
|
||||
default:
|
||||
# Read https://github.com/tgalopin/html-sanitizer/blob/master/docs/1-getting-started.md#extensions
|
||||
# to learn more about which extensions you would like to enable.
|
||||
extensions:
|
||||
- 'basic'
|
||||
- 'list'
|
||||
- 'table'
|
||||
- 'image'
|
||||
- 'code'
|
||||
# - 'iframe'
|
||||
# - 'extra'
|
||||
|
||||
# Read https://github.com/tgalopin/html-sanitizer/blob/master/docs/3-configuration-reference.md
|
||||
# to discover all the available options for each extension.
|
3
app/config/packages/mailer.yaml
Normal file
3
app/config/packages/mailer.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
mailer:
|
||||
dsn: '%env(MAILER_DSN)%'
|
8
app/config/packages/prod/deprecations.yaml
Normal file
8
app/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
app/config/packages/prod/doctrine.yaml
Normal file
20
app/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
app/config/packages/prod/monolog.yaml
Normal file
16
app/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
app/config/packages/prod/routing.yaml
Normal file
3
app/config/packages/prod/routing.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
4
app/config/packages/prod/webpack_encore.yaml
Normal file
4
app/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
|
@ -5,8 +5,3 @@ framework:
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
#default_uri: http://localhost
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
||||
|
63
app/config/packages/security.yaml
Normal file
63
app/config/packages/security.yaml
Normal file
@ -0,0 +1,63 @@
|
||||
security:
|
||||
encoders:
|
||||
# Our user class and the algorithm we'll use to encode passwords
|
||||
# 'auto' means to let Symfony choose the best possible password hasher (Argon2 or Bcrypt)
|
||||
# https://symfony.com/doc/current/security.html#c-encoding-passwords
|
||||
App\Entity\User: 'auto'
|
||||
|
||||
providers:
|
||||
# https://symfony.com/doc/current/security/user_provider.html
|
||||
# In this example, users are stored via Doctrine in the database
|
||||
# To see the users at src/App/DataFixtures/ORM/LoadFixtures.php
|
||||
# To load users from somewhere else: https://symfony.com/doc/current/security/user_provider.html#creating-a-custom-user-provider
|
||||
database_users:
|
||||
entity: { class: App\Entity\User, property: username }
|
||||
|
||||
# https://symfony.com/doc/current/security.html#a-authentication-firewalls
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
|
||||
main:
|
||||
# this firewall applies to all URLs
|
||||
pattern: ^/
|
||||
|
||||
# but the firewall does not require login on every page
|
||||
# denying access is done in access_control or in your controllers
|
||||
anonymous: true
|
||||
lazy: true
|
||||
|
||||
# The user provider to use.
|
||||
provider: database_users
|
||||
|
||||
# This allows the user to login by submitting a username and password
|
||||
# Reference: https://symfony.com/doc/current/security/form_login_setup.html
|
||||
form_login:
|
||||
# The route name that the login form submits to
|
||||
check_path: security_login
|
||||
# The name of the route where the login form lives
|
||||
# When the user tries to access a protected page, they are redirected here
|
||||
login_path: security_login
|
||||
# Secure the login form against CSRF
|
||||
# Reference: https://symfony.com/doc/current/security/csrf.html#csrf-protection-in-login-forms
|
||||
csrf_token_generator: security.csrf.token_manager
|
||||
# The page users are redirect to when there is no previous page stored in the
|
||||
# session (for example when the users access directly to the login page).
|
||||
default_target_path: blog_index
|
||||
|
||||
logout:
|
||||
# The route name the user can go to in order to logout
|
||||
path: security_logout
|
||||
# The name of the route to redirect to after logging out
|
||||
target: homepage
|
||||
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
# this is a catch-all for the admin area
|
||||
# additional security lives in the controllers
|
||||
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }
|
||||
|
||||
role_hierarchy:
|
||||
ROLE_ADMIN: ROLE_USER
|
4
app/config/packages/test/dama_doctrine_test_bundle.yaml
Normal file
4
app/config/packages/test/dama_doctrine_test_bundle.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
dama_doctrine_test:
|
||||
enable_static_connection: true
|
||||
enable_static_meta_data_cache: true
|
||||
enable_static_query_cache: true
|
4
app/config/packages/test/framework.yaml
Normal file
4
app/config/packages/test/framework.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_id: session.storage.mock_file
|
4
app/config/packages/test/mailer.yaml
Normal file
4
app/config/packages/test/mailer.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
framework:
|
||||
mailer:
|
||||
# this disables delivery of messages entirely
|
||||
dsn: 'null://null'
|
12
app/config/packages/test/monolog.yaml
Normal file
12
app/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
|
6
app/config/packages/test/security.yaml
Normal file
6
app/config/packages/test/security.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
# this configuration simplifies testing URLs protected by the security mechanism
|
||||
# See https://symfony.com/doc/current/testing/http_authentication.html
|
||||
security:
|
||||
firewalls:
|
||||
main:
|
||||
http_basic: ~
|
2
app/config/packages/test/twig.yaml
Normal file
2
app/config/packages/test/twig.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
twig:
|
||||
strict_variables: true
|
3
app/config/packages/test/validator.yaml
Normal file
3
app/config/packages/test/validator.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
6
app/config/packages/test/web_profiler.yaml
Normal file
6
app/config/packages/test/web_profiler.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
2
app/config/packages/test/webpack_encore.yaml
Normal file
2
app/config/packages/test/webpack_encore.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
#webpack_encore:
|
||||
# strict_mode: false
|
@ -1,13 +1,6 @@
|
||||
framework:
|
||||
default_locale: en
|
||||
default_locale: '%locale%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- en
|
||||
# providers:
|
||||
# crowdin:
|
||||
# dsn: '%env(CROWDIN_DSN)%'
|
||||
# loco:
|
||||
# dsn: '%env(LOCO_DSN)%'
|
||||
# lokalise:
|
||||
# dsn: '%env(LOKALISE_DSN)%'
|
||||
- '%locale%'
|
||||
|
@ -1,6 +1,5 @@
|
||||
twig:
|
||||
default_path: '%kernel.project_dir%/templates'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
form_themes:
|
||||
- 'form/layout.html.twig'
|
||||
- 'form/fields.html.twig'
|
||||
|
@ -1,13 +1,9 @@
|
||||
framework:
|
||||
validation:
|
||||
enable_annotations: true
|
||||
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\: []
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
||||
auto_mapping:
|
||||
App\Entity\: []
|
||||
|
@ -1,15 +0,0 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
@ -4,42 +4,22 @@ webpack_encore:
|
||||
# If multiple builds are defined (as shown below), you can disable the default build:
|
||||
# output_path: false
|
||||
|
||||
# Set attributes that will be rendered on all script and link tags
|
||||
script_attributes:
|
||||
defer: true
|
||||
# Uncomment (also under link_attributes) if using Turbo Drive
|
||||
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
|
||||
# 'data-turbo-track': reload
|
||||
# link_attributes:
|
||||
# Uncomment if using Turbo Drive
|
||||
# 'data-turbo-track': reload
|
||||
|
||||
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
|
||||
# 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 all rendered script and link tags automatically via the http2 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:
|
||||
# if you have multiple builds:
|
||||
# builds:
|
||||
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||
|
||||
# pass the build name as the 3rd argument to the Twig functions
|
||||
# pass "frontend" as the 3rg arg to the Twig functions
|
||||
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||
|
||||
#when@prod:
|
||||
# webpack_encore:
|
||||
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# # Available in version 1.2
|
||||
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# Put in config/packages/prod/webpack_encore.yaml
|
||||
# cache: true
|
||||
|
||||
#when@test:
|
||||
# webpack_encore:
|
||||
# strict_mode: false
|
||||
|
@ -1,3 +1,12 @@
|
||||
#index:
|
||||
# path: /
|
||||
# controller: App\Controller\DefaultController::index
|
||||
# These lines define a route using YAML configuration. The controller used by
|
||||
# the route (FrameworkBundle:Template:template) is a convenient shortcut when
|
||||
# the template can be rendered without executing any logic in your own controller.
|
||||
# See https://symfony.com/doc/current/templates.html#rendering-a-template-directly-from-a-route
|
||||
homepage:
|
||||
path: /{_locale}
|
||||
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController::templateAction
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
template: default/homepage.html.twig
|
||||
_locale: '%locale%'
|
||||
|
@ -1,7 +1,8 @@
|
||||
controllers:
|
||||
resource: ../../src/Controller/
|
||||
type: annotation
|
||||
|
||||
kernel:
|
||||
resource: ../../src/Kernel.php
|
||||
resource: '../../src/Controller/'
|
||||
type: annotation
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
3
app/config/routes/dev/framework.yaml
Normal file
3
app/config/routes/dev/framework.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
7
app/config/routes/dev/web_profiler.yaml
Normal file
7
app/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
|
@ -1,4 +0,0 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
@ -1,8 +0,0 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
@ -2,14 +2,22 @@
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
locale: 'en'
|
||||
# This parameter defines the codes of the locales (languages) enabled in the application
|
||||
app_locales: ar|en|fr|de|es|cs|nl|ru|uk|ro|pt_BR|pl|it|ja|id|ca|sl|hr|zh_CN|bg|tr|lt
|
||||
app.notifications.email_sender: anonymous@example.com
|
||||
|
||||
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.
|
||||
bind: # defines the scalar arguments once and apply them to any service defined/created in this file
|
||||
string $locales: '%app_locales%'
|
||||
string $defaultLocale: '%locale%'
|
||||
string $emailSender: '%app.notifications.email_sender%'
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
@ -19,6 +27,15 @@ services:
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
||||
- '../src/Tests/'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
App\Controller\:
|
||||
resource: '../src/Controller/'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
# when the service definition only contains arguments, you can omit the
|
||||
# 'arguments' key and define the arguments just below the service class
|
||||
App\EventSubscriber\CommentNotificationSubscriber:
|
||||
$sender: '%app.notifications.email_sender%'
|
||||
|
BIN
app/data/database.sqlite
Normal file
BIN
app/data/database.sqlite
Normal file
Binary file not shown.
BIN
app/data/database_test.sqlite
Normal file
BIN
app/data/database_test.sqlite
Normal file
Binary file not shown.
@ -1,8 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
ports:
|
||||
- "5432"
|
||||
###< doctrine/doctrine-bundle ###
|
@ -1,21 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
image: postgres:${POSTGRES_VERSION:-15}-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
# You should definitely change the password in production
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
volumes:
|
||||
- database_data:/var/lib/postgresql/data:rw
|
||||
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
|
||||
# - ./docker/db/data:/var/lib/postgresql/data:rw
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
volumes:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database_data:
|
||||
###< doctrine/doctrine-bundle ###
|
@ -1,17 +1,22 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@hotwired/stimulus": "^3.0.0",
|
||||
"@symfony/stimulus-bridge": "^3.2.0",
|
||||
"@symfony/webpack-encore": "^4.0.0",
|
||||
"core-js": "^3.23.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-notifier": "^1.15.0"
|
||||
"@fortawesome/fontawesome-free": "^5.8.1",
|
||||
"@symfony/webpack-encore": "^0.31.0",
|
||||
"bloodhound-js": "^1.2.3",
|
||||
"bootstrap-sass": "^3.3.7",
|
||||
"bootstrap-tagsinput": "^0.7.1",
|
||||
"bootswatch": "^3.3.7",
|
||||
"core-js": "^3.0.0",
|
||||
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
|
||||
"highlight.js": "^10.4.1",
|
||||
"imports-loader": "^0.8.0",
|
||||
"jquery": "^3.5.1",
|
||||
"lato-font": "^3.0.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"sass-loader": "^9.0.1",
|
||||
"typeahead.js": "^0.11.1"
|
||||
},
|
||||
"license": "UNLICENSED",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev-server": "encore dev-server",
|
||||
|
39
app/phpunit.xml.dist
Normal file
39
app/phpunit.xml.dist
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="bin/.phpunit/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<server name="APP_ENV" value="test" force="true" />
|
||||
<server name="SHELL_VERBOSITY" value="-1" />
|
||||
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
|
||||
<server name="SYMFONY_PHPUNIT_VERSION" value="8" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>src</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
|
||||
<listeners>
|
||||
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
|
||||
</listeners>
|
||||
|
||||
<extensions>
|
||||
<!-- it begins a database transaction before every testcase and rolls it back after
|
||||
the test finished, so tests can manipulate the database without affecting other tests -->
|
||||
<extension class="\DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
|
||||
</extensions>
|
||||
</phpunit>
|
66
app/public/.htaccess
Normal file
66
app/public/.htaccess
Normal file
@ -0,0 +1,66 @@
|
||||
# Use the front controller as index file. It serves as a fallback solution when
|
||||
# every other rewrite/redirect fails (e.g. in an aliased environment without
|
||||
# mod_rewrite). Additionally, this reduces the matching process for the
|
||||
# start page (path "/") because otherwise Apache will apply the rewriting rules
|
||||
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||
DirectoryIndex index.php
|
||||
|
||||
# By default, Apache does not evaluate symbolic links if you did not enable this
|
||||
# feature in your server configuration. Uncomment the following line if you
|
||||
# install assets as symlinks or if you experience problems related to symlinks
|
||||
# when compiling LESS/Sass/CoffeScript assets.
|
||||
# Options +FollowSymlinks
|
||||
|
||||
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
|
||||
# to the front controller "/index.php" but be rewritten to "/index.php/index".
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Determine the RewriteBase automatically and set it as environment variable.
|
||||
# If you are using Apache aliases to do mass virtual hosting or installed the
|
||||
# project in a subdirectory, the base path will be prepended to allow proper
|
||||
# resolution of the index.php file and to redirect to the correct URI. It will
|
||||
# work in environments without path prefix as well, providing a safe, one-size
|
||||
# fits all solution. But as you do not need it in this case, you can comment
|
||||
# the following 2 lines to eliminate the overhead.
|
||||
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
|
||||
RewriteRule .* - [E=BASE:%1]
|
||||
|
||||
# Sets the HTTP_AUTHORIZATION header removed by Apache
|
||||
RewriteCond %{HTTP:Authorization} .+
|
||||
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
|
||||
|
||||
# Redirect to URI without front controller to prevent duplicate content
|
||||
# (with and without `/index.php`). Only do this redirect on the initial
|
||||
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
|
||||
# endless redirect loop (request -> rewrite to front controller ->
|
||||
# redirect -> request -> ...).
|
||||
# So in case you get a "too many redirects" error or you always get redirected
|
||||
# to the start page because your Apache does not expose the REDIRECT_STATUS
|
||||
# environment variable, you have 2 choices:
|
||||
# - disable this feature by commenting the following 2 lines or
|
||||
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
|
||||
# following RewriteCond (best solution)
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
|
||||
|
||||
# If the requested filename exists, simply serve it.
|
||||
# We only want to let Apache serve files and not directories.
|
||||
# Rewrite all other queries to the front controller.
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||
</IfModule>
|
||||
|
||||
<IfModule !mod_rewrite.c>
|
||||
<IfModule mod_alias.c>
|
||||
# When mod_rewrite is not available, we instruct a temporary redirect of
|
||||
# the start page to the front controller explicitly so that the website
|
||||
# and the generated links can still be used.
|
||||
RedirectMatch 307 ^/$ /index.php/
|
||||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
BIN
app/public/apple-touch-icon.png
Normal file
BIN
app/public/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
app/public/favicon.ico
Normal file
BIN
app/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
@ -1,9 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
use Symfony\Component\ErrorHandler\Debug;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||
|
||||
if ($_SERVER['APP_DEBUG']) {
|
||||
umask(0000);
|
||||
|
||||
Debug::enable();
|
||||
}
|
||||
|
||||
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) {
|
||||
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
|
||||
}
|
||||
|
||||
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) {
|
||||
Request::setTrustedHosts([$trustedHosts]);
|
||||
}
|
||||
|
||||
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||
$request = Request::createFromGlobals();
|
||||
$response = $kernel->handle($request);
|
||||
$response->send();
|
||||
$kernel->terminate($request, $response);
|
||||
|
4
app/public/robots.txt
Normal file
4
app/public/robots.txt
Normal file
@ -0,0 +1,4 @@
|
||||
# www.robotstxt.org/
|
||||
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
|
||||
|
||||
User-agent: *
|
263
app/src/Command/AddUserCommand.php
Normal file
263
app/src/Command/AddUserCommand.php
Normal file
@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Utils\Validator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
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\Style\SymfonyStyle;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
/**
|
||||
* A console command that creates users and stores them in the database.
|
||||
*
|
||||
* To use this command, open a terminal window, enter into your project
|
||||
* directory and execute the following:
|
||||
*
|
||||
* $ php bin/console app:add-user
|
||||
*
|
||||
* To output detailed information, increase the command verbosity:
|
||||
*
|
||||
* $ php bin/console app:add-user -vv
|
||||
*
|
||||
* See https://symfony.com/doc/current/console.html
|
||||
*
|
||||
* We use the default services.yaml configuration, so command classes are registered as services.
|
||||
* See https://symfony.com/doc/current/console/commands_as_services.html
|
||||
*
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class AddUserCommand extends Command
|
||||
{
|
||||
// to make your command lazily loaded, configure the $defaultName static property,
|
||||
// so it will be instantiated only when the command is actually called.
|
||||
protected static $defaultName = 'app:add-user';
|
||||
|
||||
/**
|
||||
* @var SymfonyStyle
|
||||
*/
|
||||
private $io;
|
||||
|
||||
private $entityManager;
|
||||
private $passwordEncoder;
|
||||
private $validator;
|
||||
private $users;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, UserPasswordEncoderInterface $encoder, Validator $validator, UserRepository $users)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->entityManager = $em;
|
||||
$this->passwordEncoder = $encoder;
|
||||
$this->validator = $validator;
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Creates users and stores them in the database')
|
||||
->setHelp($this->getCommandHelp())
|
||||
// commands can optionally define arguments and/or options (mandatory and optional)
|
||||
// see https://symfony.com/doc/current/components/console/console_arguments.html
|
||||
->addArgument('username', InputArgument::OPTIONAL, 'The username of the new user')
|
||||
->addArgument('password', InputArgument::OPTIONAL, 'The plain password of the new user')
|
||||
->addArgument('email', InputArgument::OPTIONAL, 'The email of the new user')
|
||||
->addArgument('full-name', InputArgument::OPTIONAL, 'The full name of the new user')
|
||||
->addOption('admin', null, InputOption::VALUE_NONE, 'If set, the user is created as an administrator')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* This optional method is the first one executed for a command after configure()
|
||||
* and is useful to initialize properties based on the input arguments and options.
|
||||
*/
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
// SymfonyStyle is an optional feature that Symfony provides so you can
|
||||
// apply a consistent look to the commands of your application.
|
||||
// See https://symfony.com/doc/current/console/style.html
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is executed after initialize() and before execute(). Its purpose
|
||||
* is to check if some of the options/arguments are missing and interactively
|
||||
* ask the user for those values.
|
||||
*
|
||||
* This method is completely optional. If you are developing an internal console
|
||||
* command, you probably should not implement this method because it requires
|
||||
* quite a lot of work. However, if the command is meant to be used by external
|
||||
* users, this method is a nice way to fall back and prevent errors.
|
||||
*/
|
||||
protected function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
if (null !== $input->getArgument('username') && null !== $input->getArgument('password') && null !== $input->getArgument('email') && null !== $input->getArgument('full-name')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->io->title('Add User Command Interactive Wizard');
|
||||
$this->io->text([
|
||||
'If you prefer to not use this interactive wizard, provide the',
|
||||
'arguments required by this command as follows:',
|
||||
'',
|
||||
' $ php bin/console app:add-user username password email@example.com',
|
||||
'',
|
||||
'Now we\'ll ask you for the value of all the missing command arguments.',
|
||||
]);
|
||||
|
||||
// Ask for the username if it's not defined
|
||||
$username = $input->getArgument('username');
|
||||
if (null !== $username) {
|
||||
$this->io->text(' > <info>Username</info>: '.$username);
|
||||
} else {
|
||||
$username = $this->io->ask('Username', null, [$this->validator, 'validateUsername']);
|
||||
$input->setArgument('username', $username);
|
||||
}
|
||||
|
||||
// Ask for the password if it's not defined
|
||||
$password = $input->getArgument('password');
|
||||
if (null !== $password) {
|
||||
$this->io->text(' > <info>Password</info>: '.u('*')->repeat(u($password)->length()));
|
||||
} else {
|
||||
$password = $this->io->askHidden('Password (your type will be hidden)', [$this->validator, 'validatePassword']);
|
||||
$input->setArgument('password', $password);
|
||||
}
|
||||
|
||||
// Ask for the email if it's not defined
|
||||
$email = $input->getArgument('email');
|
||||
if (null !== $email) {
|
||||
$this->io->text(' > <info>Email</info>: '.$email);
|
||||
} else {
|
||||
$email = $this->io->ask('Email', null, [$this->validator, 'validateEmail']);
|
||||
$input->setArgument('email', $email);
|
||||
}
|
||||
|
||||
// Ask for the full name if it's not defined
|
||||
$fullName = $input->getArgument('full-name');
|
||||
if (null !== $fullName) {
|
||||
$this->io->text(' > <info>Full Name</info>: '.$fullName);
|
||||
} else {
|
||||
$fullName = $this->io->ask('Full Name', null, [$this->validator, 'validateFullName']);
|
||||
$input->setArgument('full-name', $fullName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is executed after interact() and initialize(). It usually
|
||||
* contains the logic to execute to complete this command task.
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$stopwatch = new Stopwatch();
|
||||
$stopwatch->start('add-user-command');
|
||||
|
||||
$username = $input->getArgument('username');
|
||||
$plainPassword = $input->getArgument('password');
|
||||
$email = $input->getArgument('email');
|
||||
$fullName = $input->getArgument('full-name');
|
||||
$isAdmin = $input->getOption('admin');
|
||||
|
||||
// make sure to validate the user data is correct
|
||||
$this->validateUserData($username, $plainPassword, $email, $fullName);
|
||||
|
||||
// create the user and encode its password
|
||||
$user = new User();
|
||||
$user->setFullName($fullName);
|
||||
$user->setUsername($username);
|
||||
$user->setEmail($email);
|
||||
$user->setRoles([$isAdmin ? 'ROLE_ADMIN' : 'ROLE_USER']);
|
||||
|
||||
// See https://symfony.com/doc/current/security.html#c-encoding-passwords
|
||||
$encodedPassword = $this->passwordEncoder->encodePassword($user, $plainPassword);
|
||||
$user->setPassword($encodedPassword);
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->io->success(sprintf('%s was successfully created: %s (%s)', $isAdmin ? 'Administrator user' : 'User', $user->getUsername(), $user->getEmail()));
|
||||
|
||||
$event = $stopwatch->stop('add-user-command');
|
||||
if ($output->isVerbose()) {
|
||||
$this->io->comment(sprintf('New user database id: %d / Elapsed time: %.2f ms / Consumed memory: %.2f MB', $user->getId(), $event->getDuration(), $event->getMemory() / (1024 ** 2)));
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function validateUserData($username, $plainPassword, $email, $fullName): void
|
||||
{
|
||||
// first check if a user with the same username already exists.
|
||||
$existingUser = $this->users->findOneBy(['username' => $username]);
|
||||
|
||||
if (null !== $existingUser) {
|
||||
throw new RuntimeException(sprintf('There is already a user registered with the "%s" username.', $username));
|
||||
}
|
||||
|
||||
// validate password and email if is not this input means interactive.
|
||||
$this->validator->validatePassword($plainPassword);
|
||||
$this->validator->validateEmail($email);
|
||||
$this->validator->validateFullName($fullName);
|
||||
|
||||
// check if a user with the same email already exists.
|
||||
$existingEmail = $this->users->findOneBy(['email' => $email]);
|
||||
|
||||
if (null !== $existingEmail) {
|
||||
throw new RuntimeException(sprintf('There is already a user registered with the "%s" email.', $email));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The command help is usually included in the configure() method, but when
|
||||
* it's too long, it's better to define a separate method to maintain the
|
||||
* code readability.
|
||||
*/
|
||||
private function getCommandHelp(): string
|
||||
{
|
||||
return <<<'HELP'
|
||||
The <info>%command.name%</info> command creates new users and saves them in the database:
|
||||
|
||||
<info>php %command.full_name%</info> <comment>username password email</comment>
|
||||
|
||||
By default the command creates regular users. To create administrator users,
|
||||
add the <comment>--admin</comment> option:
|
||||
|
||||
<info>php %command.full_name%</info> username password email <comment>--admin</comment>
|
||||
|
||||
If you omit any of the three required arguments, the command will ask you to
|
||||
provide the missing values:
|
||||
|
||||
# command will ask you for the email
|
||||
<info>php %command.full_name%</info> <comment>username password</comment>
|
||||
|
||||
# command will ask you for the email and password
|
||||
<info>php %command.full_name%</info> <comment>username</comment>
|
||||
|
||||
# command will ask you for all arguments
|
||||
<info>php %command.full_name%</info>
|
||||
|
||||
HELP;
|
||||
}
|
||||
}
|
132
app/src/Command/DeleteUserCommand.php
Normal file
132
app/src/Command/DeleteUserCommand.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Utils\Validator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* A console command that deletes users from the database.
|
||||
*
|
||||
* To use this command, open a terminal window, enter into your project
|
||||
* directory and execute the following:
|
||||
*
|
||||
* $ php bin/console app:delete-user
|
||||
*
|
||||
* Check out the code of the src/Command/AddUserCommand.php file for
|
||||
* the full explanation about Symfony commands.
|
||||
*
|
||||
* See https://symfony.com/doc/current/console.html
|
||||
*
|
||||
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
|
||||
*/
|
||||
class DeleteUserCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'app:delete-user';
|
||||
|
||||
/** @var SymfonyStyle */
|
||||
private $io;
|
||||
private $entityManager;
|
||||
private $validator;
|
||||
private $users;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, Validator $validator, UserRepository $users)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->entityManager = $em;
|
||||
$this->validator = $validator;
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Deletes users from the database')
|
||||
->addArgument('username', InputArgument::REQUIRED, 'The username of an existing user')
|
||||
->setHelp(<<<'HELP'
|
||||
The <info>%command.name%</info> command deletes users from the database:
|
||||
|
||||
<info>php %command.full_name%</info> <comment>username</comment>
|
||||
|
||||
If you omit the argument, the command will ask you to
|
||||
provide the missing value:
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
// SymfonyStyle is an optional feature that Symfony provides so you can
|
||||
// apply a consistent look to the commands of your application.
|
||||
// See https://symfony.com/doc/current/console/style.html
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
if (null !== $input->getArgument('username')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->io->title('Delete User Command Interactive Wizard');
|
||||
$this->io->text([
|
||||
'If you prefer to not use this interactive wizard, provide the',
|
||||
'arguments required by this command as follows:',
|
||||
'',
|
||||
' $ php bin/console app:delete-user username',
|
||||
'',
|
||||
'Now we\'ll ask you for the value of all the missing command arguments.',
|
||||
'',
|
||||
]);
|
||||
|
||||
$username = $this->io->ask('Username', null, [$this->validator, 'validateUsername']);
|
||||
$input->setArgument('username', $username);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$username = $this->validator->validateUsername($input->getArgument('username'));
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->users->findOneByUsername($username);
|
||||
|
||||
if (null === $user) {
|
||||
throw new RuntimeException(sprintf('User with username "%s" not found.', $username));
|
||||
}
|
||||
|
||||
// After an entity has been removed its in-memory state is the same
|
||||
// as before the removal, except for generated identifiers.
|
||||
// See https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#removing-entities
|
||||
$userId = $user->getId();
|
||||
|
||||
$this->entityManager->remove($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->io->success(sprintf('User "%s" (ID: %d, email: %s) was successfully deleted.', $user->getUsername(), $userId, $user->getEmail()));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
146
app/src/Command/ListUsersCommand.php
Normal file
146
app/src/Command/ListUsersCommand.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
/**
|
||||
* A console command that lists all the existing users.
|
||||
*
|
||||
* To use this command, open a terminal window, enter into your project directory
|
||||
* and execute the following:
|
||||
*
|
||||
* $ php bin/console app:list-users
|
||||
*
|
||||
* Check out the code of the src/Command/AddUserCommand.php file for
|
||||
* the full explanation about Symfony commands.
|
||||
*
|
||||
* See https://symfony.com/doc/current/console.html
|
||||
*
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class ListUsersCommand extends Command
|
||||
{
|
||||
// a good practice is to use the 'app:' prefix to group all your custom application commands
|
||||
protected static $defaultName = 'app:list-users';
|
||||
|
||||
private $mailer;
|
||||
private $emailSender;
|
||||
private $users;
|
||||
|
||||
public function __construct(MailerInterface $mailer, string $emailSender, UserRepository $users)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->mailer = $mailer;
|
||||
$this->emailSender = $emailSender;
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Lists all the existing users')
|
||||
->setHelp(<<<'HELP'
|
||||
The <info>%command.name%</info> command lists all the users registered in the application:
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
|
||||
By default the command only displays the 50 most recent users. Set the number of
|
||||
results to display with the <comment>--max-results</comment> option:
|
||||
|
||||
<info>php %command.full_name%</info> <comment>--max-results=2000</comment>
|
||||
|
||||
In addition to displaying the user list, you can also send this information to
|
||||
the email address specified in the <comment>--send-to</comment> option:
|
||||
|
||||
<info>php %command.full_name%</info> <comment>--send-to=fabien@symfony.com</comment>
|
||||
|
||||
HELP
|
||||
)
|
||||
// commands can optionally define arguments and/or options (mandatory and optional)
|
||||
// see https://symfony.com/doc/current/components/console/console_arguments.html
|
||||
->addOption('max-results', null, InputOption::VALUE_OPTIONAL, 'Limits the number of users listed', 50)
|
||||
->addOption('send-to', null, InputOption::VALUE_OPTIONAL, 'If set, the result is sent to the given email address')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is executed after initialize(). It usually contains the logic
|
||||
* to execute to complete this command task.
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$maxResults = $input->getOption('max-results');
|
||||
// Use ->findBy() instead of ->findAll() to allow result sorting and limiting
|
||||
$allUsers = $this->users->findBy([], ['id' => 'DESC'], $maxResults);
|
||||
|
||||
// Doctrine query returns an array of objects and we need an array of plain arrays
|
||||
$usersAsPlainArrays = array_map(function (User $user) {
|
||||
return [
|
||||
$user->getId(),
|
||||
$user->getFullName(),
|
||||
$user->getUsername(),
|
||||
$user->getEmail(),
|
||||
implode(', ', $user->getRoles()),
|
||||
];
|
||||
}, $allUsers);
|
||||
|
||||
// In your console commands you should always use the regular output type,
|
||||
// which outputs contents directly in the console window. However, this
|
||||
// command uses the BufferedOutput type instead, to be able to get the output
|
||||
// contents before displaying them. This is needed because the command allows
|
||||
// to send the list of users via email with the '--send-to' option
|
||||
$bufferedOutput = new BufferedOutput();
|
||||
$io = new SymfonyStyle($input, $bufferedOutput);
|
||||
$io->table(
|
||||
['ID', 'Full Name', 'Username', 'Email', 'Roles'],
|
||||
$usersAsPlainArrays
|
||||
);
|
||||
|
||||
// instead of just displaying the table of users, store its contents in a variable
|
||||
$usersAsATable = $bufferedOutput->fetch();
|
||||
$output->write($usersAsATable);
|
||||
|
||||
if (null !== $email = $input->getOption('send-to')) {
|
||||
$this->sendReport($usersAsATable, $email);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the given $contents to the $recipient email address.
|
||||
*/
|
||||
private function sendReport(string $contents, string $recipient): void
|
||||
{
|
||||
$email = (new Email())
|
||||
->from($this->emailSender)
|
||||
->to($recipient)
|
||||
->subject(sprintf('app:list-users report (%s)', date('Y-m-d H:i:s')))
|
||||
->text($contents);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
177
app/src/Controller/Admin/BlogController.php
Normal file
177
app/src/Controller/Admin/BlogController.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Post;
|
||||
use App\Form\PostType;
|
||||
use App\Repository\PostRepository;
|
||||
use App\Security\PostVoter;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage blog contents in the backend.
|
||||
*
|
||||
* Please note that the application backend is developed manually for learning
|
||||
* purposes. However, in your real Symfony application you should use any of the
|
||||
* existing bundles that let you generate ready-to-use backends without effort.
|
||||
*
|
||||
* See http://knpbundles.com/keyword/admin
|
||||
*
|
||||
* @Route("/admin/post")
|
||||
* @IsGranted("ROLE_ADMIN")
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class BlogController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* Lists all Post entities.
|
||||
*
|
||||
* This controller responds to two different routes with the same URL:
|
||||
* * 'admin_post_index' is the route with a name that follows the same
|
||||
* structure as the rest of the controllers of this class.
|
||||
* * 'admin_index' is a nice shortcut to the backend homepage. This allows
|
||||
* to create simpler links in the templates. Moreover, in the future we
|
||||
* could move this annotation to any other controller while maintaining
|
||||
* the route name and therefore, without breaking any existing link.
|
||||
*
|
||||
* @Route("/", methods="GET", name="admin_index")
|
||||
* @Route("/", methods="GET", name="admin_post_index")
|
||||
*/
|
||||
public function index(PostRepository $posts): Response
|
||||
{
|
||||
$authorPosts = $posts->findBy(['author' => $this->getUser()], ['publishedAt' => 'DESC']);
|
||||
|
||||
return $this->render('admin/blog/index.html.twig', ['posts' => $authorPosts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Post entity.
|
||||
*
|
||||
* @Route("/new", methods="GET|POST", name="admin_post_new")
|
||||
*
|
||||
* NOTE: the Method annotation is optional, but it's a recommended practice
|
||||
* to constraint the HTTP methods each controller responds to (by default
|
||||
* it responds to all methods).
|
||||
*/
|
||||
public function new(Request $request): Response
|
||||
{
|
||||
$post = new Post();
|
||||
$post->setAuthor($this->getUser());
|
||||
|
||||
// See https://symfony.com/doc/current/form/multiple_buttons.html
|
||||
$form = $this->createForm(PostType::class, $post)
|
||||
->add('saveAndCreateNew', SubmitType::class);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
// the isSubmitted() method is completely optional because the other
|
||||
// isValid() method already checks whether the form is submitted.
|
||||
// However, we explicitly add it to improve code readability.
|
||||
// See https://symfony.com/doc/current/forms.html#processing-forms
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->persist($post);
|
||||
$em->flush();
|
||||
|
||||
// Flash messages are used to notify the user about the result of the
|
||||
// actions. They are deleted automatically from the session as soon
|
||||
// as they are accessed.
|
||||
// See https://symfony.com/doc/current/controller.html#flash-messages
|
||||
$this->addFlash('success', 'post.created_successfully');
|
||||
|
||||
if ($form->get('saveAndCreateNew')->isClicked()) {
|
||||
return $this->redirectToRoute('admin_post_new');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_post_index');
|
||||
}
|
||||
|
||||
return $this->render('admin/blog/new.html.twig', [
|
||||
'post' => $post,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and displays a Post entity.
|
||||
*
|
||||
* @Route("/{id<\d+>}", methods="GET", name="admin_post_show")
|
||||
*/
|
||||
public function show(Post $post): Response
|
||||
{
|
||||
// This security check can also be performed
|
||||
// using an annotation: @IsGranted("show", subject="post", message="Posts can only be shown to their authors.")
|
||||
$this->denyAccessUnlessGranted(PostVoter::SHOW, $post, 'Posts can only be shown to their authors.');
|
||||
|
||||
return $this->render('admin/blog/show.html.twig', [
|
||||
'post' => $post,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a form to edit an existing Post entity.
|
||||
*
|
||||
* @Route("/{id<\d+>}/edit", methods="GET|POST", name="admin_post_edit")
|
||||
* @IsGranted("edit", subject="post", message="Posts can only be edited by their authors.")
|
||||
*/
|
||||
public function edit(Request $request, Post $post): Response
|
||||
{
|
||||
$form = $this->createForm(PostType::class, $post);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
|
||||
$this->addFlash('success', 'post.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute('admin_post_edit', ['id' => $post->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('admin/blog/edit.html.twig', [
|
||||
'post' => $post,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a Post entity.
|
||||
*
|
||||
* @Route("/{id}/delete", methods="POST", name="admin_post_delete")
|
||||
* @IsGranted("delete", subject="post")
|
||||
*/
|
||||
public function delete(Request $request, Post $post): Response
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('delete', $request->request->get('token'))) {
|
||||
return $this->redirectToRoute('admin_post_index');
|
||||
}
|
||||
|
||||
// Delete the tags associated with this blog post. This is done automatically
|
||||
// by Doctrine, except for SQLite (the database used in this application)
|
||||
// because foreign key support is not enabled by default in SQLite
|
||||
$post->getTags()->clear();
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->remove($post);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'post.deleted_successfully');
|
||||
|
||||
return $this->redirectToRoute('admin_post_index');
|
||||
}
|
||||
}
|
170
app/src/Controller/BlogController.php
Normal file
170
app/src/Controller/BlogController.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Comment;
|
||||
use App\Entity\Post;
|
||||
use App\Event\CommentCreatedEvent;
|
||||
use App\Form\CommentType;
|
||||
use App\Repository\PostRepository;
|
||||
use App\Repository\TagRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage blog contents in the public part of the site.
|
||||
*
|
||||
* @Route("/blog")
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class BlogController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/", defaults={"page": "1", "_format"="html"}, methods="GET", name="blog_index")
|
||||
* @Route("/rss.xml", defaults={"page": "1", "_format"="xml"}, methods="GET", name="blog_rss")
|
||||
* @Route("/page/{page<[1-9]\d*>}", defaults={"_format"="html"}, methods="GET", name="blog_index_paginated")
|
||||
* @Cache(smaxage="10")
|
||||
*
|
||||
* NOTE: For standard formats, Symfony will also automatically choose the best
|
||||
* Content-Type header for the response.
|
||||
* See https://symfony.com/doc/current/routing.html#special-parameters
|
||||
*/
|
||||
public function index(Request $request, int $page, string $_format, PostRepository $posts, TagRepository $tags): Response
|
||||
{
|
||||
$tag = null;
|
||||
if ($request->query->has('tag')) {
|
||||
$tag = $tags->findOneBy(['name' => $request->query->get('tag')]);
|
||||
}
|
||||
$latestPosts = $posts->findLatest($page, $tag);
|
||||
|
||||
// Every template name also has two extensions that specify the format and
|
||||
// engine for that template.
|
||||
// See https://symfony.com/doc/current/templates.html#template-naming
|
||||
return $this->render('blog/index.'.$_format.'.twig', [
|
||||
'paginator' => $latestPosts,
|
||||
'tagName' => $tag ? $tag->getName() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/posts/{slug}", methods="GET", name="blog_post")
|
||||
*
|
||||
* NOTE: The $post controller argument is automatically injected by Symfony
|
||||
* after performing a database query looking for a Post with the 'slug'
|
||||
* value given in the route.
|
||||
* See https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
|
||||
*/
|
||||
public function postShow(Post $post): Response
|
||||
{
|
||||
// Symfony's 'dump()' function is an improved version of PHP's 'var_dump()' but
|
||||
// it's not available in the 'prod' environment to prevent leaking sensitive information.
|
||||
// It can be used both in PHP files and Twig templates, but it requires to
|
||||
// have enabled the DebugBundle. Uncomment the following line to see it in action:
|
||||
//
|
||||
// dump($post, $this->getUser(), new \DateTime());
|
||||
|
||||
return $this->render('blog/post_show.html.twig', ['post' => $post]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/comment/{postSlug}/new", methods="POST", name="comment_new")
|
||||
* @IsGranted("IS_AUTHENTICATED_FULLY")
|
||||
* @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
|
||||
*
|
||||
* NOTE: The ParamConverter mapping is required because the route parameter
|
||||
* (postSlug) doesn't match any of the Doctrine entity properties (slug).
|
||||
* See https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html#doctrine-converter
|
||||
*/
|
||||
public function commentNew(Request $request, Post $post, EventDispatcherInterface $eventDispatcher): Response
|
||||
{
|
||||
$comment = new Comment();
|
||||
$comment->setAuthor($this->getUser());
|
||||
$post->addComment($comment);
|
||||
|
||||
$form = $this->createForm(CommentType::class, $comment);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->persist($comment);
|
||||
$em->flush();
|
||||
|
||||
// When an event is dispatched, Symfony notifies it to all the listeners
|
||||
// and subscribers registered to it. Listeners can modify the information
|
||||
// passed in the event and they can even modify the execution flow, so
|
||||
// there's no guarantee that the rest of this controller will be executed.
|
||||
// See https://symfony.com/doc/current/components/event_dispatcher.html
|
||||
$eventDispatcher->dispatch(new CommentCreatedEvent($comment));
|
||||
|
||||
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
|
||||
}
|
||||
|
||||
return $this->render('blog/comment_form_error.html.twig', [
|
||||
'post' => $post,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This controller is called directly via the render() function in the
|
||||
* blog/post_show.html.twig template. That's why it's not needed to define
|
||||
* a route name for it.
|
||||
*
|
||||
* The "id" of the Post is passed in and then turned into a Post object
|
||||
* automatically by the ParamConverter.
|
||||
*/
|
||||
public function commentForm(Post $post): Response
|
||||
{
|
||||
$form = $this->createForm(CommentType::class);
|
||||
|
||||
return $this->render('blog/_comment_form.html.twig', [
|
||||
'post' => $post,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/search", methods="GET", name="blog_search")
|
||||
*/
|
||||
public function search(Request $request, PostRepository $posts): Response
|
||||
{
|
||||
$query = $request->query->get('q', '');
|
||||
$limit = $request->query->get('l', 10);
|
||||
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
return $this->render('blog/search.html.twig', ['query' => $query]);
|
||||
}
|
||||
|
||||
$foundPosts = $posts->findBySearchQuery($query, $limit);
|
||||
|
||||
$results = [];
|
||||
foreach ($foundPosts as $post) {
|
||||
$results[] = [
|
||||
'title' => htmlspecialchars($post->getTitle(), ENT_COMPAT | ENT_HTML5),
|
||||
'date' => $post->getPublishedAt()->format('M d, Y'),
|
||||
'author' => htmlspecialchars($post->getAuthor()->getFullName(), ENT_COMPAT | ENT_HTML5),
|
||||
'summary' => htmlspecialchars($post->getSummary(), ENT_COMPAT | ENT_HTML5),
|
||||
'url' => $this->generateUrl('blog_post', ['slug' => $post->getSlug()]),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->json($results);
|
||||
}
|
||||
}
|
69
app/src/Controller/SecurityController.php
Normal file
69
app/src/Controller/SecurityController.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
|
||||
/**
|
||||
* Controller used to manage the application security.
|
||||
* See https://symfony.com/doc/current/security/form_login_setup.html.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
use TargetPathTrait;
|
||||
|
||||
/**
|
||||
* @Route("/login", name="security_login")
|
||||
*/
|
||||
public function login(Request $request, Security $security, AuthenticationUtils $helper): Response
|
||||
{
|
||||
// if user is already logged in, don't display the login page again
|
||||
if ($security->isGranted('ROLE_USER')) {
|
||||
return $this->redirectToRoute('blog_index');
|
||||
}
|
||||
|
||||
// this statement solves an edge-case: if you change the locale in the login
|
||||
// page, after a successful login you are redirected to a page in the previous
|
||||
// locale. This code regenerates the referrer URL whenever the login page is
|
||||
// browsed, to ensure that its locale is always the current one.
|
||||
$this->saveTargetPath($request->getSession(), 'main', $this->generateUrl('admin_index'));
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
// last username entered by the user (if any)
|
||||
'last_username' => $helper->getLastUsername(),
|
||||
// last authentication error (if any)
|
||||
'error' => $helper->getLastAuthenticationError(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the route the user can use to logout.
|
||||
*
|
||||
* But, this will never be executed. Symfony will intercept this first
|
||||
* and handle the logout automatically. See logout in config/packages/security.yaml
|
||||
*
|
||||
* @Route("/logout", name="security_logout")
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
throw new \Exception('This should never be reached!');
|
||||
}
|
||||
}
|
79
app/src/Controller/UserController.php
Normal file
79
app/src/Controller/UserController.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Form\Type\ChangePasswordType;
|
||||
use App\Form\UserType;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
/**
|
||||
* Controller used to manage current user.
|
||||
*
|
||||
* @Route("/profile")
|
||||
* @IsGranted("ROLE_USER")
|
||||
*
|
||||
* @author Romain Monteil <monteil.romain@gmail.com>
|
||||
*/
|
||||
class UserController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/edit", methods="GET|POST", name="user_edit")
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$form = $this->createForm(UserType::class, $user);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
|
||||
$this->addFlash('success', 'user.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute('user_edit');
|
||||
}
|
||||
|
||||
return $this->render('user/edit.html.twig', [
|
||||
'user' => $user,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/change-password", methods="GET|POST", name="user_change_password")
|
||||
*/
|
||||
public function changePassword(Request $request, UserPasswordEncoderInterface $encoder): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$form = $this->createForm(ChangePasswordType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user->setPassword($encoder->encodePassword($user, $form->get('newPassword')->getData()));
|
||||
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
|
||||
return $this->redirectToRoute('security_logout');
|
||||
}
|
||||
|
||||
return $this->render('user/change_password.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
241
app/src/DataFixtures/AppFixtures.php
Normal file
241
app/src/DataFixtures/AppFixtures.php
Normal file
@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Entity\Comment;
|
||||
use App\Entity\Post;
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
class AppFixtures extends Fixture
|
||||
{
|
||||
private $passwordEncoder;
|
||||
private $slugger;
|
||||
|
||||
public function __construct(UserPasswordEncoderInterface $passwordEncoder, SluggerInterface $slugger)
|
||||
{
|
||||
$this->passwordEncoder = $passwordEncoder;
|
||||
$this->slugger = $slugger;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$this->loadUsers($manager);
|
||||
$this->loadTags($manager);
|
||||
$this->loadPosts($manager);
|
||||
}
|
||||
|
||||
private function loadUsers(ObjectManager $manager): void
|
||||
{
|
||||
foreach ($this->getUserData() as [$fullname, $username, $password, $email, $roles]) {
|
||||
$user = new User();
|
||||
$user->setFullName($fullname);
|
||||
$user->setUsername($username);
|
||||
$user->setPassword($this->passwordEncoder->encodePassword($user, $password));
|
||||
$user->setEmail($email);
|
||||
$user->setRoles($roles);
|
||||
|
||||
$manager->persist($user);
|
||||
$this->addReference($username, $user);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function loadTags(ObjectManager $manager): void
|
||||
{
|
||||
foreach ($this->getTagData() as $index => $name) {
|
||||
$tag = new Tag();
|
||||
$tag->setName($name);
|
||||
|
||||
$manager->persist($tag);
|
||||
$this->addReference('tag-'.$name, $tag);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function loadPosts(ObjectManager $manager): void
|
||||
{
|
||||
foreach ($this->getPostData() as [$title, $slug, $summary, $content, $publishedAt, $author, $tags]) {
|
||||
$post = new Post();
|
||||
$post->setTitle($title);
|
||||
$post->setSlug($slug);
|
||||
$post->setSummary($summary);
|
||||
$post->setContent($content);
|
||||
$post->setPublishedAt($publishedAt);
|
||||
$post->setAuthor($author);
|
||||
$post->addTag(...$tags);
|
||||
|
||||
foreach (range(1, 5) as $i) {
|
||||
$comment = new Comment();
|
||||
$comment->setAuthor($this->getReference('john_user'));
|
||||
$comment->setContent($this->getRandomText(random_int(255, 512)));
|
||||
$comment->setPublishedAt(new \DateTime('now + '.$i.'seconds'));
|
||||
|
||||
$post->addComment($comment);
|
||||
}
|
||||
|
||||
$manager->persist($post);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function getUserData(): array
|
||||
{
|
||||
return [
|
||||
// $userData = [$fullname, $username, $password, $email, $roles];
|
||||
['Jane Doe', 'jane_admin', 'kitten', 'jane_admin@symfony.com', ['ROLE_ADMIN']],
|
||||
['Tom Doe', 'tom_admin', 'kitten', 'tom_admin@symfony.com', ['ROLE_ADMIN']],
|
||||
['John Doe', 'john_user', 'kitten', 'john_user@symfony.com', ['ROLE_USER']],
|
||||
];
|
||||
}
|
||||
|
||||
private function getTagData(): array
|
||||
{
|
||||
return [
|
||||
'lorem',
|
||||
'ipsum',
|
||||
'consectetur',
|
||||
'adipiscing',
|
||||
'incididunt',
|
||||
'labore',
|
||||
'voluptate',
|
||||
'dolore',
|
||||
'pariatur',
|
||||
];
|
||||
}
|
||||
|
||||
private function getPostData()
|
||||
{
|
||||
$posts = [];
|
||||
foreach ($this->getPhrases() as $i => $title) {
|
||||
// $postData = [$title, $slug, $summary, $content, $publishedAt, $author, $tags, $comments];
|
||||
$posts[] = [
|
||||
$title,
|
||||
$this->slugger->slug($title)->lower(),
|
||||
$this->getRandomText(),
|
||||
$this->getPostContent(),
|
||||
new \DateTime('now - '.$i.'days'),
|
||||
// Ensure that the first post is written by Jane Doe to simplify tests
|
||||
$this->getReference(['jane_admin', 'tom_admin'][0 === $i ? 0 : random_int(0, 1)]),
|
||||
$this->getRandomTags(),
|
||||
];
|
||||
}
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
private function getPhrases(): array
|
||||
{
|
||||
return [
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit',
|
||||
'Pellentesque vitae velit ex',
|
||||
'Mauris dapibus risus quis suscipit vulputate',
|
||||
'Eros diam egestas libero eu vulputate risus',
|
||||
'In hac habitasse platea dictumst',
|
||||
'Morbi tempus commodo mattis',
|
||||
'Ut suscipit posuere justo at vulputate',
|
||||
'Ut eleifend mauris et risus ultrices egestas',
|
||||
'Aliquam sodales odio id eleifend tristique',
|
||||
'Urna nisl sollicitudin id varius orci quam id turpis',
|
||||
'Nulla porta lobortis ligula vel egestas',
|
||||
'Curabitur aliquam euismod dolor non ornare',
|
||||
'Sed varius a risus eget aliquam',
|
||||
'Nunc viverra elit ac laoreet suscipit',
|
||||
'Pellentesque et sapien pulvinar consectetur',
|
||||
'Ubi est barbatus nix',
|
||||
'Abnobas sunt hilotaes de placidus vita',
|
||||
'Ubi est audax amicitia',
|
||||
'Eposs sunt solems de superbus fortis',
|
||||
'Vae humani generis',
|
||||
'Diatrias tolerare tanquam noster caesium',
|
||||
'Teres talis saepe tractare de camerarius flavum sensorem',
|
||||
'Silva de secundus galatae demitto quadra',
|
||||
'Sunt accentores vitare salvus flavum parses',
|
||||
'Potus sensim ad ferox abnoba',
|
||||
'Sunt seculaes transferre talis camerarius fluctuies',
|
||||
'Era brevis ratione est',
|
||||
'Sunt torquises imitari velox mirabilis medicinaes',
|
||||
'Mineralis persuadere omnes finises desiderium',
|
||||
'Bassus fatalis classiss virtualiter transferre de flavum',
|
||||
];
|
||||
}
|
||||
|
||||
private function getRandomText(int $maxLength = 255): string
|
||||
{
|
||||
$phrases = $this->getPhrases();
|
||||
shuffle($phrases);
|
||||
|
||||
do {
|
||||
$text = u('. ')->join($phrases)->append('.');
|
||||
array_pop($phrases);
|
||||
} while ($text->length() > $maxLength);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
private function getPostContent(): string
|
||||
{
|
||||
return <<<'MARKDOWN'
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit, sed do eiusmod tempor
|
||||
incididunt ut labore et **dolore magna aliqua**: Duis aute irure dolor in
|
||||
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
|
||||
deserunt mollit anim id est laborum.
|
||||
|
||||
* Ut enim ad minim veniam
|
||||
* Quis nostrud exercitation *ullamco laboris*
|
||||
* Nisi ut aliquip ex ea commodo consequat
|
||||
|
||||
Praesent id fermentum lorem. Ut est lorem, fringilla at accumsan nec, euismod at
|
||||
nunc. Aenean mattis sollicitudin mattis. Nullam pulvinar vestibulum bibendum.
|
||||
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
|
||||
himenaeos. Fusce nulla purus, gravida ac interdum ut, blandit eget ex. Duis a
|
||||
luctus dolor.
|
||||
|
||||
Integer auctor massa maximus nulla scelerisque accumsan. *Aliquam ac malesuada*
|
||||
ex. Pellentesque tortor magna, vulputate eu vulputate ut, venenatis ac lectus.
|
||||
Praesent ut lacinia sem. Mauris a lectus eget felis mollis feugiat. Quisque
|
||||
efficitur, mi ut semper pulvinar, urna urna blandit massa, eget tincidunt augue
|
||||
nulla vitae est.
|
||||
|
||||
Ut posuere aliquet tincidunt. Aliquam erat volutpat. **Class aptent taciti**
|
||||
sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi
|
||||
arcu orci, gravida eget aliquam eu, suscipit et ante. Morbi vulputate metus vel
|
||||
ipsum finibus, ut dapibus massa feugiat. Vestibulum vel lobortis libero. Sed
|
||||
tincidunt tellus et viverra scelerisque. Pellentesque tincidunt cursus felis.
|
||||
Sed in egestas erat.
|
||||
|
||||
Aliquam pulvinar interdum massa, vel ullamcorper ante consectetur eu. Vestibulum
|
||||
lacinia ac enim vel placerat. Integer pulvinar magna nec dui malesuada, nec
|
||||
congue nisl dictum. Donec mollis nisl tortor, at congue erat consequat a. Nam
|
||||
tempus elit porta, blandit elit vel, viverra lorem. Sed sit amet tellus
|
||||
tincidunt, faucibus nisl in, aliquet libero.
|
||||
MARKDOWN;
|
||||
}
|
||||
|
||||
private function getRandomTags(): array
|
||||
{
|
||||
$tagNames = $this->getTagData();
|
||||
shuffle($tagNames);
|
||||
$selectedTags = \array_slice($tagNames, 0, random_int(2, 4));
|
||||
|
||||
return array_map(function ($tagName) { return $this->getReference('tag-'.$tagName); }, $selectedTags);
|
||||
}
|
||||
}
|
0
app/src/Entity/.gitignore
vendored
0
app/src/Entity/.gitignore
vendored
138
app/src/Entity/Comment.php
Normal file
138
app/src/Entity/Comment.php
Normal file
@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use function Symfony\Component\String\u;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="symfony_demo_comment")
|
||||
*
|
||||
* Defines the properties of the Comment entity to represent the blog comments.
|
||||
* See https://symfony.com/doc/current/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* Tip: if you have an existing database, you can generate these entity class automatically.
|
||||
* See https://symfony.com/doc/current/doctrine/reverse_engineering.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class Comment
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Post
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Post", inversedBy="comments")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private $post;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text")
|
||||
* @Assert\NotBlank(message="comment.blank")
|
||||
* @Assert\Length(
|
||||
* min=5,
|
||||
* minMessage="comment.too_short",
|
||||
* max=10000,
|
||||
* maxMessage="comment.too_long"
|
||||
* )
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private $publishedAt;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private $author;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->publishedAt = new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Assert\IsTrue(message="comment.is_spam")
|
||||
*/
|
||||
public function isLegitComment(): bool
|
||||
{
|
||||
$containsInvalidCharacters = null !== u($this->content)->indexOf('@');
|
||||
|
||||
return !$containsInvalidCharacters;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(string $content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function getPublishedAt(): \DateTime
|
||||
{
|
||||
return $this->publishedAt;
|
||||
}
|
||||
|
||||
public function setPublishedAt(\DateTime $publishedAt): void
|
||||
{
|
||||
$this->publishedAt = $publishedAt;
|
||||
}
|
||||
|
||||
public function getAuthor(): ?User
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function setAuthor(User $author): void
|
||||
{
|
||||
$this->author = $author;
|
||||
}
|
||||
|
||||
public function getPost(): ?Post
|
||||
{
|
||||
return $this->post;
|
||||
}
|
||||
|
||||
public function setPost(Post $post): void
|
||||
{
|
||||
$this->post = $post;
|
||||
}
|
||||
}
|
226
app/src/Entity/Post.php
Normal file
226
app/src/Entity/Post.php
Normal file
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="App\Repository\PostRepository")
|
||||
* @ORM\Table(name="symfony_demo_post")
|
||||
* @UniqueEntity(fields={"slug"}, errorPath="title", message="post.slug_unique")
|
||||
*
|
||||
* Defines the properties of the Post entity to represent the blog posts.
|
||||
*
|
||||
* See https://symfony.com/doc/current/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* Tip: if you have an existing database, you can generate these entity class automatically.
|
||||
* See https://symfony.com/doc/current/doctrine/reverse_engineering.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class Post
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string")
|
||||
* @Assert\NotBlank
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string")
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string")
|
||||
* @Assert\NotBlank(message="post.blank_summary")
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $summary;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text")
|
||||
* @Assert\NotBlank(message="post.blank_content")
|
||||
* @Assert\Length(min=10, minMessage="post.too_short_content")
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private $publishedAt;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private $author;
|
||||
|
||||
/**
|
||||
* @var Comment[]|Collection
|
||||
*
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity="Comment",
|
||||
* mappedBy="post",
|
||||
* orphanRemoval=true,
|
||||
* cascade={"persist"}
|
||||
* )
|
||||
* @ORM\OrderBy({"publishedAt": "DESC"})
|
||||
*/
|
||||
private $comments;
|
||||
|
||||
/**
|
||||
* @var Tag[]|Collection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="App\Entity\Tag", cascade={"persist"})
|
||||
* @ORM\JoinTable(name="symfony_demo_post_tag")
|
||||
* @ORM\OrderBy({"name": "ASC"})
|
||||
* @Assert\Count(max="4", maxMessage="post.too_many_tags")
|
||||
*/
|
||||
private $tags;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->publishedAt = new \DateTime();
|
||||
$this->comments = new ArrayCollection();
|
||||
$this->tags = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(?string $title): void
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function getSlug(): ?string
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function setSlug(string $slug): void
|
||||
{
|
||||
$this->slug = $slug;
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(?string $content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function getPublishedAt(): \DateTime
|
||||
{
|
||||
return $this->publishedAt;
|
||||
}
|
||||
|
||||
public function setPublishedAt(\DateTime $publishedAt): void
|
||||
{
|
||||
$this->publishedAt = $publishedAt;
|
||||
}
|
||||
|
||||
public function getAuthor(): ?User
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function setAuthor(User $author): void
|
||||
{
|
||||
$this->author = $author;
|
||||
}
|
||||
|
||||
public function getComments(): Collection
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
public function addComment(Comment $comment): void
|
||||
{
|
||||
$comment->setPost($this);
|
||||
if (!$this->comments->contains($comment)) {
|
||||
$this->comments->add($comment);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeComment(Comment $comment): void
|
||||
{
|
||||
$this->comments->removeElement($comment);
|
||||
}
|
||||
|
||||
public function getSummary(): ?string
|
||||
{
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
public function setSummary(?string $summary): void
|
||||
{
|
||||
$this->summary = $summary;
|
||||
}
|
||||
|
||||
public function addTag(Tag ...$tags): void
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (!$this->tags->contains($tag)) {
|
||||
$this->tags->add($tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function removeTag(Tag $tag): void
|
||||
{
|
||||
$this->tags->removeElement($tag);
|
||||
}
|
||||
|
||||
public function getTags(): Collection
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
}
|
75
app/src/Entity/Tag.php
Normal file
75
app/src/Entity/Tag.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="symfony_demo_tag")
|
||||
*
|
||||
* Defines the properties of the Tag entity to represent the post tags.
|
||||
*
|
||||
* See https://symfony.com/doc/current/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class Tag implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", unique=true)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
// This entity implements JsonSerializable (http://php.net/manual/en/class.jsonserializable.php)
|
||||
// so this method is used to customize its JSON representation when json_encode()
|
||||
// is called, for example in tags|json_encode (templates/form/fields.html.twig)
|
||||
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
188
app/src/Entity/User.php
Normal file
188
app/src/Entity/User.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
|
||||
* @ORM\Table(name="symfony_demo_user")
|
||||
*
|
||||
* Defines the properties of the User entity to represent the application users.
|
||||
* See https://symfony.com/doc/current/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* Tip: if you have an existing database, you can generate these entity class automatically.
|
||||
* See https://symfony.com/doc/current/doctrine/reverse_engineering.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class User implements UserInterface, \Serializable
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string")
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $fullName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", unique=true)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=50)
|
||||
*/
|
||||
private $username;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", unique=true)
|
||||
* @Assert\Email()
|
||||
*/
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string")
|
||||
*/
|
||||
private $password;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*
|
||||
* @ORM\Column(type="json")
|
||||
*/
|
||||
private $roles = [];
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setFullName(string $fullName): void
|
||||
{
|
||||
$this->fullName = $fullName;
|
||||
}
|
||||
|
||||
public function getFullName(): ?string
|
||||
{
|
||||
return $this->fullName;
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function setUsername(string $username): void
|
||||
{
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): void
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): void
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the roles or permissions granted to the user for security.
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
|
||||
// guarantees that a user always has at least one role for security
|
||||
if (empty($roles)) {
|
||||
$roles[] = 'ROLE_USER';
|
||||
}
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
public function setRoles(array $roles): void
|
||||
{
|
||||
$this->roles = $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the salt that was originally used to encode the password.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSalt(): ?string
|
||||
{
|
||||
// We're using bcrypt in security.yaml to encode the password, so
|
||||
// the salt value is built-in and and you don't have to generate one
|
||||
// See https://en.wikipedia.org/wiki/Bcrypt
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes sensitive data from the user.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// if you had a plainPassword property, you'd nullify it here
|
||||
// $this->plainPassword = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function serialize(): string
|
||||
{
|
||||
// add $this->salt too if you don't use Bcrypt or Argon2i
|
||||
return serialize([$this->id, $this->username, $this->password]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function unserialize($serialized): void
|
||||
{
|
||||
// add $this->salt too if you don't use Bcrypt or Argon2i
|
||||
[$this->id, $this->username, $this->password] = unserialize($serialized, ['allowed_classes' => false]);
|
||||
}
|
||||
}
|
30
app/src/Event/CommentCreatedEvent.php
Normal file
30
app/src/Event/CommentCreatedEvent.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Comment;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class CommentCreatedEvent extends Event
|
||||
{
|
||||
protected $comment;
|
||||
|
||||
public function __construct(Comment $comment)
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
public function getComment(): Comment
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
}
|
101
app/src/EventSubscriber/CheckRequirementsSubscriber.php
Normal file
101
app/src/EventSubscriber/CheckRequirementsSubscriber.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\ConsoleEvents;
|
||||
use Symfony\Component\Console\Event\ConsoleErrorEvent;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* This application uses by default an SQLite database to store its information.
|
||||
* That's why the 'sqlite3' extension must be enabled in PHP. This event
|
||||
* subscriber listens to console events and in case of an exception caused by
|
||||
* a disabled 'sqlite3' extension, it displays a meaningful error message.
|
||||
*
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class CheckRequirementsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
// Event Subscribers must define this method to declare the events they
|
||||
// listen to. You can listen to several events, execute more than one method
|
||||
// for each event and set the priority of each event too.
|
||||
// See https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-subscriber
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
// Errors are one of the events defined by the Console. See the
|
||||
// rest here: https://symfony.com/doc/current/components/console/events.html
|
||||
ConsoleEvents::ERROR => 'handleConsoleError',
|
||||
// See: https://symfony.com/doc/current/components/http_kernel.html#component-http-kernel-event-table
|
||||
KernelEvents::EXCEPTION => 'handleKernelException',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks if there has been an error in a command related to
|
||||
* the database and then, it checks if the 'sqlite3' PHP extension is enabled
|
||||
* or not to display a better error message.
|
||||
*/
|
||||
public function handleConsoleError(ConsoleErrorEvent $event): void
|
||||
{
|
||||
$commandNames = ['doctrine:fixtures:load', 'doctrine:database:create', 'doctrine:schema:create', 'doctrine:database:drop'];
|
||||
|
||||
if ($event->getCommand() && \in_array($event->getCommand()->getName(), $commandNames, true)) {
|
||||
if ($this->isSQLitePlatform() && !\extension_loaded('sqlite3')) {
|
||||
$io = new SymfonyStyle($event->getInput(), $event->getOutput());
|
||||
$io->error('This command requires to have the "sqlite3" PHP extension enabled because, by default, the Symfony Demo application uses SQLite to store its information.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks if the triggered exception is related to the database
|
||||
* and then, it checks if the required 'sqlite3' PHP extension is enabled.
|
||||
*/
|
||||
public function handleKernelException(ExceptionEvent $event): void
|
||||
{
|
||||
$exception = $event->getThrowable();
|
||||
// Since any exception thrown during a Twig template rendering is wrapped
|
||||
// in a Twig_Error_Runtime, we must get the original exception.
|
||||
$previousException = $exception->getPrevious();
|
||||
|
||||
// Driver exception may happen in controller or in twig template rendering
|
||||
$isDriverException = ($exception instanceof DriverException || $previousException instanceof DriverException);
|
||||
|
||||
// Check if SQLite is enabled
|
||||
if ($isDriverException && $this->isSQLitePlatform() && !\extension_loaded('sqlite3')) {
|
||||
$event->setThrowable(new \Exception('PHP extension "sqlite3" must be enabled because, by default, the Symfony Demo application uses SQLite to store its information.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the application is using SQLite as its database.
|
||||
*/
|
||||
private function isSQLitePlatform(): bool
|
||||
{
|
||||
$databasePlatform = $this->entityManager->getConnection()->getDatabasePlatform();
|
||||
|
||||
return $databasePlatform ? 'sqlite' === $databasePlatform->getName() : false;
|
||||
}
|
||||
}
|
79
app/src/EventSubscriber/CommentNotificationSubscriber.php
Normal file
79
app/src/EventSubscriber/CommentNotificationSubscriber.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\Comment;
|
||||
use App\Event\CommentCreatedEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Notifies post's author about new comments.
|
||||
*
|
||||
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
|
||||
*/
|
||||
class CommentNotificationSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private $mailer;
|
||||
private $translator;
|
||||
private $urlGenerator;
|
||||
private $sender;
|
||||
|
||||
public function __construct(MailerInterface $mailer, UrlGeneratorInterface $urlGenerator, TranslatorInterface $translator, string $sender)
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->translator = $translator;
|
||||
$this->sender = $sender;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CommentCreatedEvent::class => 'onCommentCreated',
|
||||
];
|
||||
}
|
||||
|
||||
public function onCommentCreated(CommentCreatedEvent $event): void
|
||||
{
|
||||
/** @var Comment $comment */
|
||||
$comment = $event->getComment();
|
||||
$post = $comment->getPost();
|
||||
|
||||
$linkToPost = $this->urlGenerator->generate('blog_post', [
|
||||
'slug' => $post->getSlug(),
|
||||
'_fragment' => 'comment_'.$comment->getId(),
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$subject = $this->translator->trans('notification.comment_created');
|
||||
$body = $this->translator->trans('notification.comment_created.description', [
|
||||
'%title%' => $post->getTitle(),
|
||||
'%link%' => $linkToPost,
|
||||
]);
|
||||
|
||||
// See https://symfony.com/doc/current/mailer.html
|
||||
$email = (new Email())
|
||||
->from($this->sender)
|
||||
->to($post->getAuthor()->getEmail())
|
||||
->subject($subject)
|
||||
->html($body)
|
||||
;
|
||||
|
||||
// In config/packages/dev/mailer.yaml the delivery of messages is disabled.
|
||||
// That's why in the development environment you won't actually receive any email.
|
||||
// However, you can inspect the contents of those unsent emails using the debug toolbar.
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
51
app/src/EventSubscriber/ControllerSubscriber.php
Normal file
51
app/src/EventSubscriber/ControllerSubscriber.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Twig\SourceCodeExtension;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ControllerEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* Defines the method that 'listens' to the 'kernel.controller' event, which is
|
||||
* triggered whenever a controller is executed in the application.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class ControllerSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private $twigExtension;
|
||||
|
||||
public function __construct(SourceCodeExtension $twigExtension)
|
||||
{
|
||||
$this->twigExtension = $twigExtension;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::CONTROLLER => 'registerCurrentController',
|
||||
];
|
||||
}
|
||||
|
||||
public function registerCurrentController(ControllerEvent $event): void
|
||||
{
|
||||
// this check is needed because in Symfony a request can perform any
|
||||
// number of sub-requests. See
|
||||
// https://symfony.com/doc/current/components/http_kernel.html#sub-requests
|
||||
if ($event->isMasterRequest()) {
|
||||
$this->twigExtension->setController($event->getController());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
/**
|
||||
* When visiting the homepage, this listener redirects the user to the most
|
||||
* appropriate localized version according to the browser settings.
|
||||
*
|
||||
* See https://symfony.com/doc/current/components/http_kernel.html#the-kernel-request-event
|
||||
*
|
||||
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
|
||||
*/
|
||||
class RedirectToPreferredLocaleSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private $urlGenerator;
|
||||
private $locales;
|
||||
private $defaultLocale;
|
||||
|
||||
public function __construct(UrlGeneratorInterface $urlGenerator, string $locales, string $defaultLocale = null)
|
||||
{
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
|
||||
$this->locales = explode('|', trim($locales));
|
||||
if (empty($this->locales)) {
|
||||
throw new \UnexpectedValueException('The list of supported locales must not be empty.');
|
||||
}
|
||||
|
||||
$this->defaultLocale = $defaultLocale ?: $this->locales[0];
|
||||
|
||||
if (!\in_array($this->defaultLocale, $this->locales, true)) {
|
||||
throw new \UnexpectedValueException(sprintf('The default locale ("%s") must be one of "%s".', $this->defaultLocale, $locales));
|
||||
}
|
||||
|
||||
// Add the default locale at the first position of the array,
|
||||
// because Symfony\HttpFoundation\Request::getPreferredLanguage
|
||||
// returns the first element when no an appropriate language is found
|
||||
array_unshift($this->locales, $this->defaultLocale);
|
||||
$this->locales = array_unique($this->locales);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => 'onKernelRequest',
|
||||
];
|
||||
}
|
||||
|
||||
public function onKernelRequest(RequestEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
// Ignore sub-requests and all URLs but the homepage
|
||||
if (!$event->isMasterRequest() || '/' !== $request->getPathInfo()) {
|
||||
return;
|
||||
}
|
||||
// Ignore requests from referrers with the same HTTP host in order to prevent
|
||||
// changing language for users who possibly already selected it for this application.
|
||||
$referrer = $request->headers->get('referer');
|
||||
if (null !== $referrer && u($referrer)->ignoreCase()->startsWith($request->getSchemeAndHttpHost())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$preferredLanguage = $request->getPreferredLanguage($this->locales);
|
||||
|
||||
if ($preferredLanguage !== $this->defaultLocale) {
|
||||
$response = new RedirectResponse($this->urlGenerator->generate('homepage', ['_locale' => $preferredLanguage]));
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user