Compare commits
3 Commits
master
...
symfony_de
Author | SHA1 | Date | |
---|---|---|---|
3751003188 | |||
72fd1cf02d | |||
f8cfbebd26 |
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,8 +1,6 @@
|
||||
# Comment to include Symfony project
|
||||
# app
|
||||
#app/*
|
||||
!app/.keep
|
||||
|
||||
# Ignore Database datas
|
||||
data
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
|
16
README.md
16
README.md
@ -56,21 +56,6 @@ Le dépôt utilise plusieurs branches qui peuvent être utilisée selon le point
|
||||
* Control migrations: `bin/console doctrine:migrations:status`
|
||||
* ...
|
||||
|
||||
## Yarn
|
||||
|
||||
* installation de webpack-encore: `composer require symfony/webpack-encore-bundle`
|
||||
* pour finir il faut installer et lancer yarn:
|
||||
* un script bash `docker-node.sh` lancera nodejs dans un conteneur quand on a besoin de yarn:
|
||||
* au premier lancement du container node:14, j'initialise la mise en place:
|
||||
* `npm install yarn`
|
||||
* `npm install --force`
|
||||
* puis je lance (ce qu'il faudra faire à chaque fois):
|
||||
* `yarn encore dev` avec éventuellement l'option `--watch`
|
||||
|
||||
## Sass
|
||||
|
||||
* dans le container node, installation de sass: `npm install sass-loader@^11.0.0 sass --save-dev`
|
||||
|
||||
|
||||
---
|
||||
|
||||
@ -98,4 +83,5 @@ Le dépôt utilise plusieurs branches qui peuvent être utilisée selon le point
|
||||
* To clear volumes: `docker volume rm $(docker volume ls -q)`
|
||||
* To clear networks: `docker network rm $(docker network ls | tail -n+2 | awk '{if($2 !~ /bridge|none|host/){ print $1 }}')`
|
||||
|
||||
Disclaimer: This project has been generated on phpdocker.io
|
||||
|
||||
|
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
|
34
app/.env
Normal file
34
app/.env
Normal file
@ -0,0 +1,34 @@
|
||||
# In all environments, the following files are loaded if they exist,
|
||||
# the latter taking precedence over the former:
|
||||
#
|
||||
# * .env contains default values for the environment variables needed by the app
|
||||
# * .env.local uncommitted file with local overrides
|
||||
# * .env.$APP_ENV committed environment-specific defaults
|
||||
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||
#
|
||||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
#
|
||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=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%/data/database.sqlite
|
||||
DATABASE_URL="postgresql://postgres:secret@db:5432/postgres?serverVersion=12&charset=utf8"
|
||||
###< 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
|
8
app/.gitignore
vendored
8
app/.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
/.env
|
||||
/public/build/fonts/glyphicons-*
|
||||
/public/build/images/glyphicons-*
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
@ -10,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).
|
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,15 +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';
|
||||
|
||||
// SCSS
|
||||
import './styles/app.scss';
|
||||
|
||||
// start the Stimulus application
|
||||
import './bootstrap';
|
8
app/assets/bootstrap.js
vendored
8
app/assets/bootstrap.js
vendored
@ -1,8 +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,
|
||||
/\.(j|t)sx?$/
|
||||
));
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"controllers": [],
|
||||
"entrypoints": []
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { Controller } from '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;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
|
||||
h1 {
|
||||
font-size: 400%;
|
||||
}
|
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,34 +1,59 @@
|
||||
{
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "dev",
|
||||
"type": "project",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"replace": {
|
||||
"symfony/polyfill-php70": "*",
|
||||
"symfony/polyfill-php72": "*"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"composer/package-versions-deprecated": "1.11.99.1",
|
||||
"doctrine/doctrine-bundle": "^2.2",
|
||||
"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.8",
|
||||
"sensio/framework-extra-bundle": "^6.1",
|
||||
"symfony/console": "5.2.*",
|
||||
"symfony/dotenv": "5.2.*",
|
||||
"symfony/flex": "^1.3.1",
|
||||
"symfony/form": "5.2.*",
|
||||
"symfony/framework-bundle": "5.2.*",
|
||||
"symfony/maker-bundle": "^1.29",
|
||||
"symfony/proxy-manager-bridge": "5.2.*",
|
||||
"symfony/security-csrf": "5.2.*",
|
||||
"symfony/twig-bundle": "^5.2",
|
||||
"symfony/validator": "5.2.*",
|
||||
"symfony/webpack-encore-bundle": "^1.11",
|
||||
"symfony/yaml": "5.2.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^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": {
|
||||
"optimize-autoloader": true,
|
||||
"platform": {
|
||||
"php": "7.2.9"
|
||||
},
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
},
|
||||
@ -44,15 +69,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"
|
||||
@ -66,14 +86,7 @@
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "5.2.*"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/debug-bundle": "5.2.*",
|
||||
"symfony/stopwatch": "^5.2",
|
||||
"symfony/var-dumper": "5.2.*",
|
||||
"symfony/web-profiler-bundle": "^5.2"
|
||||
"allow-contrib": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3199
app/composer.lock
generated
3199
app/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -2,13 +2,18 @@
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => 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],
|
||||
];
|
||||
|
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"]
|
@ -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: '13'
|
||||
#server_version: '5.7'
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
|
@ -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: true
|
||||
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.
|
||||
@ -11,7 +11,17 @@ framework:
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
|
||||
#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
|
||||
|
||||
# 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"
|
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"]
|
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/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: ~
|
6
app/config/packages/translation.yaml
Normal file
6
app/config/packages/translation.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
framework:
|
||||
default_locale: '%locale%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- '%locale%'
|
@ -1,3 +1,5 @@
|
||||
twig:
|
||||
default_path: '%kernel.project_dir%/templates'
|
||||
form_themes: ['bootstrap_4_layout.html.twig']
|
||||
form_themes:
|
||||
- 'form/layout.html.twig'
|
||||
- 'form/fields.html.twig'
|
||||
|
@ -1,8 +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\: []
|
||||
auto_mapping:
|
||||
App\Entity\: []
|
||||
|
@ -4,21 +4,16 @@ 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
|
||||
# link_attributes:
|
||||
|
||||
# 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:
|
||||
# pass "frontend" as the 3rg arg to the Twig functions
|
||||
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||
|
@ -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%'
|
||||
|
@ -4,12 +4,20 @@
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
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
|
||||
@ -27,5 +35,7 @@ services:
|
||||
resource: '../src/Controller/'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
# 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%'
|
||||
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20210218191150 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SEQUENCE beer_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE beer (id INT NOT NULL, name VARCHAR(255) NOT NULL, alcool DOUBLE PRECISION DEFAULT NULL, PRIMARY KEY(id))');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SCHEMA public');
|
||||
$this->addSql('DROP SEQUENCE beer_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE beer');
|
||||
}
|
||||
}
|
8650
app/package-lock.json
generated
8650
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,23 +1,27 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@symfony/stimulus-bridge": "^2.0.0",
|
||||
"@symfony/webpack-encore": "^1.0.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",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"sass": "^1.32.7",
|
||||
"sass-loader": "^11.0.1",
|
||||
"stimulus": "^2.0.0",
|
||||
"webpack-notifier": "^1.6.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",
|
||||
"dev": "encore dev",
|
||||
"watch": "encore dev --watch",
|
||||
"build": "encore production --progress"
|
||||
},
|
||||
"dependencies": {
|
||||
"yarn": "^1.22.10"
|
||||
}
|
||||
}
|
||||
|
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 |
@ -15,6 +15,14 @@ if ($_SERVER['APP_DEBUG']) {
|
||||
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);
|
||||
|
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');
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Beer;
|
||||
use App\Form\BeerType;
|
||||
use App\Repository\BeerRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/beer")
|
||||
*/
|
||||
class BeerController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/", name="beer_index", methods={"GET"})
|
||||
*/
|
||||
public function index(BeerRepository $beerRepository): Response
|
||||
{
|
||||
return $this->render('beer/index.html.twig', [
|
||||
'beers' => $beerRepository->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="beer_new", methods={"GET","POST"})
|
||||
*/
|
||||
public function new(Request $request): Response
|
||||
{
|
||||
$beer = new Beer();
|
||||
$form = $this->createForm(BeerType::class, $beer);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($beer);
|
||||
$entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('beer_index');
|
||||
}
|
||||
|
||||
return $this->render('beer/new.html.twig', [
|
||||
'beer' => $beer,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="beer_show", methods={"GET"})
|
||||
*/
|
||||
public function show(Beer $beer): Response
|
||||
{
|
||||
return $this->render('beer/show.html.twig', [
|
||||
'beer' => $beer,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit", name="beer_edit", methods={"GET","POST"})
|
||||
*/
|
||||
public function edit(Request $request, Beer $beer): Response
|
||||
{
|
||||
$form = $this->createForm(BeerType::class, $beer);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
|
||||
return $this->redirectToRoute('beer_index');
|
||||
}
|
||||
|
||||
return $this->render('beer/edit.html.twig', [
|
||||
'beer' => $beer,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="beer_delete", methods={"DELETE"})
|
||||
*/
|
||||
public function delete(Request $request, Beer $beer): Response
|
||||
{
|
||||
if ($this->isCsrfTokenValid('delete'.$beer->getId(), $request->request->get('_token'))) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($beer);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('beer_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
@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\BeerRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=BeerRepository::class)
|
||||
*/
|
||||
class Beer
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="float", nullable=true)
|
||||
*/
|
||||
private $alcool;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAlcool(): ?float
|
||||
{
|
||||
return $this->alcool;
|
||||
}
|
||||
|
||||
public function setAlcool(?float $alcool): self
|
||||
{
|
||||
$this->alcool = $alcool;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Beer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class BeerType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('name')
|
||||
->add('alcool')
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Beer::class,
|
||||
]);
|
||||
}
|
||||
}
|
59
app/src/Form/CommentType.php
Normal file
59
app/src/Form/CommentType.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?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\Form;
|
||||
|
||||
use App\Entity\Comment;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the form used to create and manipulate blog comments. Although in this
|
||||
* case the form is trivial and we could build it inside the controller, a good
|
||||
* practice is to always define your forms as classes.
|
||||
*
|
||||
* See https://symfony.com/doc/current/forms.html#creating-form-classes
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class CommentType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// By default, form fields include the 'required' attribute, which enables
|
||||
// the client-side form validation. This means that you can't test the
|
||||
// server-side validation errors from the browser. To temporarily disable
|
||||
// this validation, set the 'required' attribute to 'false':
|
||||
// $builder->add('content', null, ['required' => false]);
|
||||
|
||||
$builder
|
||||
->add('content', TextareaType::class, [
|
||||
'help' => 'help.comment_content',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Comment::class,
|
||||
]);
|
||||
}
|
||||
}
|
79
app/src/Form/DataTransformer/TagArrayToStringTransformer.php
Normal file
79
app/src/Form/DataTransformer/TagArrayToStringTransformer.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\Form\DataTransformer;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Repository\TagRepository;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
/**
|
||||
* This data transformer is used to translate the array of tags into a comma separated format
|
||||
* that can be displayed and managed by Bootstrap-tagsinput js plugin (and back on submit).
|
||||
*
|
||||
* See https://symfony.com/doc/current/form/data_transformers.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
* @author Jonathan Boyer <contact@grafikart.fr>
|
||||
*/
|
||||
class TagArrayToStringTransformer implements DataTransformerInterface
|
||||
{
|
||||
private $tags;
|
||||
|
||||
public function __construct(TagRepository $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function transform($tags): string
|
||||
{
|
||||
// The value received is an array of Tag objects generated with
|
||||
// Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::transform()
|
||||
// The value returned is a string that concatenates the string representation of those objects
|
||||
|
||||
/* @var Tag[] $tags */
|
||||
return implode(',', $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reverseTransform($string): array
|
||||
{
|
||||
if (null === $string || u($string)->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$names = array_filter(array_unique(array_map('trim', u($string)->split(','))));
|
||||
|
||||
// Get the current tags and find the new ones that should be created.
|
||||
$tags = $this->tags->findBy([
|
||||
'name' => $names,
|
||||
]);
|
||||
$newNames = array_diff($names, $tags);
|
||||
foreach ($newNames as $name) {
|
||||
$tag = new Tag();
|
||||
$tag->setName($name);
|
||||
$tags[] = $tag;
|
||||
|
||||
// There's no need to persist these new tags because Doctrine does that automatically
|
||||
// thanks to the cascade={"persist"} option in the App\Entity\Post::$tags property.
|
||||
}
|
||||
|
||||
// Return an array of tags to transform them back into a Doctrine Collection.
|
||||
// See Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
|
||||
return $tags;
|
||||
}
|
||||
}
|
100
app/src/Form/PostType.php
Normal file
100
app/src/Form/PostType.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?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\Form;
|
||||
|
||||
use App\Entity\Post;
|
||||
use App\Form\Type\DateTimePickerType;
|
||||
use App\Form\Type\TagsInputType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
|
||||
/**
|
||||
* Defines the form used to create and manipulate blog posts.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class PostType extends AbstractType
|
||||
{
|
||||
private $slugger;
|
||||
|
||||
// Form types are services, so you can inject other services in them if needed
|
||||
public function __construct(SluggerInterface $slugger)
|
||||
{
|
||||
$this->slugger = $slugger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// For the full reference of options defined by each form field type
|
||||
// see https://symfony.com/doc/current/reference/forms/types.html
|
||||
|
||||
// By default, form fields include the 'required' attribute, which enables
|
||||
// the client-side form validation. This means that you can't test the
|
||||
// server-side validation errors from the browser. To temporarily disable
|
||||
// this validation, set the 'required' attribute to 'false':
|
||||
// $builder->add('title', null, ['required' => false, ...]);
|
||||
|
||||
$builder
|
||||
->add('title', null, [
|
||||
'attr' => ['autofocus' => true],
|
||||
'label' => 'label.title',
|
||||
])
|
||||
->add('summary', TextareaType::class, [
|
||||
'help' => 'help.post_summary',
|
||||
'label' => 'label.summary',
|
||||
])
|
||||
->add('content', null, [
|
||||
'attr' => ['rows' => 20],
|
||||
'help' => 'help.post_content',
|
||||
'label' => 'label.content',
|
||||
])
|
||||
->add('publishedAt', DateTimePickerType::class, [
|
||||
'label' => 'label.published_at',
|
||||
'help' => 'help.post_publication',
|
||||
])
|
||||
->add('tags', TagsInputType::class, [
|
||||
'label' => 'label.tags',
|
||||
'required' => false,
|
||||
])
|
||||
// form events let you modify information or fields at different steps
|
||||
// of the form handling process.
|
||||
// See https://symfony.com/doc/current/form/events.html
|
||||
->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
|
||||
/** @var Post */
|
||||
$post = $event->getData();
|
||||
if (null !== $postTitle = $post->getTitle()) {
|
||||
$post->setSlug($this->slugger->slug($postTitle)->lower());
|
||||
}
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Post::class,
|
||||
]);
|
||||
}
|
||||
}
|
62
app/src/Form/Type/ChangePasswordType.php
Normal file
62
app/src/Form/Type/ChangePasswordType.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?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\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* Defines the custom form field type used to change user's password.
|
||||
*
|
||||
* @author Romain Monteil <monteil.romain@gmail.com>
|
||||
*/
|
||||
class ChangePasswordType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('currentPassword', PasswordType::class, [
|
||||
'constraints' => [
|
||||
new UserPassword(),
|
||||
],
|
||||
'label' => 'label.current_password',
|
||||
'attr' => [
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
])
|
||||
->add('newPassword', RepeatedType::class, [
|
||||
'type' => PasswordType::class,
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
new Length([
|
||||
'min' => 5,
|
||||
'max' => 128,
|
||||
]),
|
||||
],
|
||||
'first_options' => [
|
||||
'label' => 'label.new_password',
|
||||
],
|
||||
'second_options' => [
|
||||
'label' => 'label.new_password_confirm',
|
||||
],
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
68
app/src/Form/Type/DateTimePickerType.php
Normal file
68
app/src/Form/Type/DateTimePickerType.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?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\Form\Type;
|
||||
|
||||
use App\Utils\MomentFormatConverter;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
/**
|
||||
* Defines the custom form field type used to manipulate datetime values across
|
||||
* Bootstrap Date\Time Picker javascript plugin.
|
||||
*
|
||||
* See https://symfony.com/doc/current/form/create_custom_field_type.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class DateTimePickerType extends AbstractType
|
||||
{
|
||||
private $formatConverter;
|
||||
|
||||
public function __construct(MomentFormatConverter $converter)
|
||||
{
|
||||
$this->formatConverter = $converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['attr']['data-date-format'] = $this->formatConverter->convert($options['format']);
|
||||
$view->vars['attr']['data-date-locale'] = u(\Locale::getDefault())->replace('_', '-')->lower();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'widget' => 'single_text',
|
||||
// if true, the browser will display the native date picker widget
|
||||
// however, this app uses a custom JavaScript widget, so it must be set to false
|
||||
'html5' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return DateTimeType::class;
|
||||
}
|
||||
}
|
70
app/src/Form/Type/TagsInputType.php
Normal file
70
app/src/Form/Type/TagsInputType.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?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\Form\Type;
|
||||
|
||||
use App\Form\DataTransformer\TagArrayToStringTransformer;
|
||||
use App\Repository\TagRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
|
||||
/**
|
||||
* Defines the custom form field type used to manipulate tags values across
|
||||
* Bootstrap-tagsinput javascript plugin.
|
||||
*
|
||||
* See https://symfony.com/doc/current/form/create_custom_field_type.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class TagsInputType extends AbstractType
|
||||
{
|
||||
private $tags;
|
||||
|
||||
public function __construct(TagRepository $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
// The Tag collection must be transformed into a comma separated string.
|
||||
// We could create a custom transformer to do Collection <-> string in one step,
|
||||
// but here we're doing the transformation in two steps (Collection <-> array <-> string)
|
||||
// and reuse the existing CollectionToArrayTransformer.
|
||||
->addModelTransformer(new CollectionToArrayTransformer(), true)
|
||||
->addModelTransformer(new TagArrayToStringTransformer($this->tags), true)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['tags'] = $this->tags->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return TextType::class;
|
||||
}
|
||||
}
|
65
app/src/Form/UserType.php
Normal file
65
app/src/Form/UserType.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?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\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the form used to edit an user.
|
||||
*
|
||||
* @author Romain Monteil <monteil.romain@gmail.com>
|
||||
*/
|
||||
class UserType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// For the full reference of options defined by each form field type
|
||||
// see https://symfony.com/doc/current/reference/forms/types.html
|
||||
|
||||
// By default, form fields include the 'required' attribute, which enables
|
||||
// the client-side form validation. This means that you can't test the
|
||||
// server-side validation errors from the browser. To temporarily disable
|
||||
// this validation, set the 'required' attribute to 'false':
|
||||
// $builder->add('title', null, ['required' => false, ...]);
|
||||
|
||||
$builder
|
||||
->add('username', TextType::class, [
|
||||
'label' => 'label.username',
|
||||
'disabled' => true,
|
||||
])
|
||||
->add('fullName', TextType::class, [
|
||||
'label' => 'label.fullname',
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'label.email',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,5 +1,14 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
|
117
app/src/Pagination/Paginator.php
Normal file
117
app/src/Pagination/Paginator.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?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\Pagination;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder as DoctrineQueryBuilder;
|
||||
use Doctrine\ORM\Tools\Pagination\CountWalker;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
|
||||
|
||||
/**
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class Paginator
|
||||
{
|
||||
/**
|
||||
* Use constants to define configuration options that rarely change instead
|
||||
* of specifying them under parameters section in config/services.yaml file.
|
||||
*
|
||||
* See https://symfony.com/doc/current/best_practices.html#use-constants-to-define-options-that-rarely-change
|
||||
*/
|
||||
public const PAGE_SIZE = 10;
|
||||
|
||||
private $queryBuilder;
|
||||
private $currentPage;
|
||||
private $pageSize;
|
||||
private $results;
|
||||
private $numResults;
|
||||
|
||||
public function __construct(DoctrineQueryBuilder $queryBuilder, int $pageSize = self::PAGE_SIZE)
|
||||
{
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
$this->pageSize = $pageSize;
|
||||
}
|
||||
|
||||
public function paginate(int $page = 1): self
|
||||
{
|
||||
$this->currentPage = max(1, $page);
|
||||
$firstResult = ($this->currentPage - 1) * $this->pageSize;
|
||||
|
||||
$query = $this->queryBuilder
|
||||
->setFirstResult($firstResult)
|
||||
->setMaxResults($this->pageSize)
|
||||
->getQuery();
|
||||
|
||||
if (0 === \count($this->queryBuilder->getDQLPart('join'))) {
|
||||
$query->setHint(CountWalker::HINT_DISTINCT, false);
|
||||
}
|
||||
|
||||
$paginator = new DoctrinePaginator($query, true);
|
||||
|
||||
$useOutputWalkers = \count($this->queryBuilder->getDQLPart('having') ?: []) > 0;
|
||||
$paginator->setUseOutputWalkers($useOutputWalkers);
|
||||
|
||||
$this->results = $paginator->getIterator();
|
||||
$this->numResults = $paginator->count();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCurrentPage(): int
|
||||
{
|
||||
return $this->currentPage;
|
||||
}
|
||||
|
||||
public function getLastPage(): int
|
||||
{
|
||||
return (int) ceil($this->numResults / $this->pageSize);
|
||||
}
|
||||
|
||||
public function getPageSize(): int
|
||||
{
|
||||
return $this->pageSize;
|
||||
}
|
||||
|
||||
public function hasPreviousPage(): bool
|
||||
{
|
||||
return $this->currentPage > 1;
|
||||
}
|
||||
|
||||
public function getPreviousPage(): int
|
||||
{
|
||||
return max(1, $this->currentPage - 1);
|
||||
}
|
||||
|
||||
public function hasNextPage(): bool
|
||||
{
|
||||
return $this->currentPage < $this->getLastPage();
|
||||
}
|
||||
|
||||
public function getNextPage(): int
|
||||
{
|
||||
return min($this->getLastPage(), $this->currentPage + 1);
|
||||
}
|
||||
|
||||
public function hasToPaginate(): bool
|
||||
{
|
||||
return $this->numResults > $this->pageSize;
|
||||
}
|
||||
|
||||
public function getNumResults(): int
|
||||
{
|
||||
return $this->numResults;
|
||||
}
|
||||
|
||||
public function getResults(): \Traversable
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
}
|
0
app/src/Repository/.gitignore
vendored
0
app/src/Repository/.gitignore
vendored
@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Beer;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @method Beer|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Beer|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Beer[] findAll()
|
||||
* @method Beer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class BeerRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Beer::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Beer[] Returns an array of Beer objects
|
||||
// */
|
||||
/*
|
||||
public function findByExampleField($value)
|
||||
{
|
||||
return $this->createQueryBuilder('b')
|
||||
->andWhere('b.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->orderBy('b.id', 'ASC')
|
||||
->setMaxResults(10)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public function findOneBySomeField($value): ?Beer
|
||||
{
|
||||
return $this->createQueryBuilder('b')
|
||||
->andWhere('b.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
}
|
97
app/src/Repository/PostRepository.php
Normal file
97
app/src/Repository/PostRepository.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?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\Repository;
|
||||
|
||||
use App\Entity\Post;
|
||||
use App\Entity\Tag;
|
||||
use App\Pagination\Paginator;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
/**
|
||||
* This custom Doctrine repository contains some methods which are useful when
|
||||
* querying for blog post information.
|
||||
*
|
||||
* See https://symfony.com/doc/current/doctrine.html#querying-for-objects-the-repository
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class PostRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Post::class);
|
||||
}
|
||||
|
||||
public function findLatest(int $page = 1, Tag $tag = null): Paginator
|
||||
{
|
||||
$qb = $this->createQueryBuilder('p')
|
||||
->addSelect('a', 't')
|
||||
->innerJoin('p.author', 'a')
|
||||
->leftJoin('p.tags', 't')
|
||||
->where('p.publishedAt <= :now')
|
||||
->orderBy('p.publishedAt', 'DESC')
|
||||
->setParameter('now', new \DateTime())
|
||||
;
|
||||
|
||||
if (null !== $tag) {
|
||||
$qb->andWhere(':tag MEMBER OF p.tags')
|
||||
->setParameter('tag', $tag);
|
||||
}
|
||||
|
||||
return (new Paginator($qb))->paginate($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Post[]
|
||||
*/
|
||||
public function findBySearchQuery(string $query, int $limit = Paginator::PAGE_SIZE): array
|
||||
{
|
||||
$searchTerms = $this->extractSearchTerms($query);
|
||||
|
||||
if (0 === \count($searchTerms)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$queryBuilder = $this->createQueryBuilder('p');
|
||||
|
||||
foreach ($searchTerms as $key => $term) {
|
||||
$queryBuilder
|
||||
->orWhere('p.title LIKE :t_'.$key)
|
||||
->setParameter('t_'.$key, '%'.$term.'%')
|
||||
;
|
||||
}
|
||||
|
||||
return $queryBuilder
|
||||
->orderBy('p.publishedAt', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the search string into an array of search terms.
|
||||
*/
|
||||
private function extractSearchTerms(string $searchQuery): array
|
||||
{
|
||||
$searchQuery = u($searchQuery)->replaceMatches('/[[:space:]]+/', ' ')->trim();
|
||||
$terms = array_unique($searchQuery->split(' '));
|
||||
|
||||
// ignore the search terms that are too short
|
||||
return array_filter($terms, function ($term) {
|
||||
return 2 <= $term->length();
|
||||
});
|
||||
}
|
||||
}
|
33
app/src/Repository/TagRepository.php
Normal file
33
app/src/Repository/TagRepository.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?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\Repository;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* This custom Doctrine repository is empty because so far we don't need any custom
|
||||
* method to query for application user information. But it's always a good practice
|
||||
* to define a custom repository that will be used when the application grows.
|
||||
*
|
||||
* See https://symfony.com/doc/current/doctrine.html#querying-for-objects-the-repository
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class TagRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Tag::class);
|
||||
}
|
||||
}
|
34
app/src/Repository/UserRepository.php
Normal file
34
app/src/Repository/UserRepository.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?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\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* This custom Doctrine repository is empty because so far we don't need any custom
|
||||
* method to query for application user information. But it's always a good practice
|
||||
* to define a custom repository that will be used when the application grows.
|
||||
*
|
||||
* See https://symfony.com/doc/current/doctrine.html#querying-for-objects-the-repository
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class UserRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, User::class);
|
||||
}
|
||||
}
|
63
app/src/Security/PostVoter.php
Normal file
63
app/src/Security/PostVoter.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?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\Security;
|
||||
|
||||
use App\Entity\Post;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/**
|
||||
* It grants or denies permissions for actions related to blog posts (such as
|
||||
* showing, editing and deleting posts).
|
||||
*
|
||||
* See https://symfony.com/doc/current/security/voters.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class PostVoter extends Voter
|
||||
{
|
||||
// Defining these constants is overkill for this simple application, but for real
|
||||
// applications, it's a recommended practice to avoid relying on "magic strings"
|
||||
public const DELETE = 'delete';
|
||||
public const EDIT = 'edit';
|
||||
public const SHOW = 'show';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function supports(string $attribute, $subject): bool
|
||||
{
|
||||
// this voter is only executed for three specific permissions on Post objects
|
||||
return $subject instanceof Post && \in_array($attribute, [self::SHOW, self::EDIT, self::DELETE], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param Post $post
|
||||
*/
|
||||
protected function voteOnAttribute(string $attribute, $post, TokenInterface $token): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
// the user must be logged in; if not, deny permission
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// the logic of this voter is pretty simple: if the logged user is the
|
||||
// author of the given blog post, grant permission; otherwise, deny it.
|
||||
// (the supports() method guarantees that $post is a Post object)
|
||||
return $user === $post->getAuthor();
|
||||
}
|
||||
}
|
65
app/src/Twig/AppExtension.php
Normal file
65
app/src/Twig/AppExtension.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?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\Twig;
|
||||
|
||||
use Symfony\Component\Intl\Locales;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* See https://symfony.com/doc/current/templating/twig_extension.html.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
* @author Julien ITARD <julienitard@gmail.com>
|
||||
*/
|
||||
class AppExtension extends AbstractExtension
|
||||
{
|
||||
private $localeCodes;
|
||||
private $locales;
|
||||
|
||||
public function __construct(string $locales)
|
||||
{
|
||||
$localeCodes = explode('|', $locales);
|
||||
sort($localeCodes);
|
||||
$this->localeCodes = $localeCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('locales', [$this, 'getLocales']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the list of codes of the locales (languages) enabled in the
|
||||
* application and returns an array with the name of each locale written
|
||||
* in its own language (e.g. English, Français, Español, etc.).
|
||||
*/
|
||||
public function getLocales(): array
|
||||
{
|
||||
if (null !== $this->locales) {
|
||||
return $this->locales;
|
||||
}
|
||||
|
||||
$this->locales = [];
|
||||
foreach ($this->localeCodes as $localeCode) {
|
||||
$this->locales[] = ['code' => $localeCode, 'name' => Locales::getName($localeCode, $localeCode)];
|
||||
}
|
||||
|
||||
return $this->locales;
|
||||
}
|
||||
}
|
137
app/src/Twig/SourceCodeExtension.php
Normal file
137
app/src/Twig/SourceCodeExtension.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?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\Twig;
|
||||
|
||||
use function Symfony\Component\String\u;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* CAUTION: this is an extremely advanced Twig extension. It's used to get the
|
||||
* source code of the controller and the template used to render the current
|
||||
* page. If you are starting with Symfony, don't look at this code and consider
|
||||
* studying instead the code of the src/App/Twig/AppExtension.php extension.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class SourceCodeExtension extends AbstractExtension
|
||||
{
|
||||
private $controller;
|
||||
|
||||
public function setController(?callable $controller)
|
||||
{
|
||||
$this->controller = $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('show_source_code', [$this, 'showSourceCode'], ['is_safe' => ['html'], 'needs_environment' => true]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|TemplateWrapper|array $template
|
||||
*/
|
||||
public function showSourceCode(Environment $twig, $template): string
|
||||
{
|
||||
return $twig->render('debug/source_code.html.twig', [
|
||||
'controller' => $this->getController(),
|
||||
'template' => $this->getTemplateSource($twig->resolveTemplate($template)),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getController(): ?array
|
||||
{
|
||||
// this happens for example for exceptions (404 errors, etc.)
|
||||
if (null === $this->controller) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$method = $this->getCallableReflector($this->controller);
|
||||
|
||||
$classCode = file($method->getFileName());
|
||||
$methodCode = \array_slice($classCode, $method->getStartLine() - 1, $method->getEndLine() - $method->getStartLine() + 1);
|
||||
$controllerCode = ' '.$method->getDocComment()."\n".implode('', $methodCode);
|
||||
|
||||
return [
|
||||
'file_path' => $method->getFileName(),
|
||||
'starting_line' => $method->getStartLine(),
|
||||
'source_code' => $this->unindentCode($controllerCode),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reflector for a callable.
|
||||
*
|
||||
* This logic is copied from Symfony\Component\HttpKernel\Controller\ControllerResolver::getArguments
|
||||
*/
|
||||
private function getCallableReflector(callable $callable): \ReflectionFunctionAbstract
|
||||
{
|
||||
if (\is_array($callable)) {
|
||||
return new \ReflectionMethod($callable[0], $callable[1]);
|
||||
}
|
||||
|
||||
if (\is_object($callable) && !$callable instanceof \Closure) {
|
||||
$r = new \ReflectionObject($callable);
|
||||
|
||||
return $r->getMethod('__invoke');
|
||||
}
|
||||
|
||||
return new \ReflectionFunction($callable);
|
||||
}
|
||||
|
||||
private function getTemplateSource(TemplateWrapper $template): array
|
||||
{
|
||||
$templateSource = $template->getSourceContext();
|
||||
|
||||
return [
|
||||
// Twig templates are not always stored in files (they can be stored
|
||||
// in a database for example). However, for the needs of the Symfony
|
||||
// Demo app, we consider that all templates are stored in files and
|
||||
// that their file paths can be obtained through the source context.
|
||||
'file_path' => $templateSource->getPath(),
|
||||
'starting_line' => 1,
|
||||
'source_code' => $templateSource->getCode(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that "unindents" the given $code when all its lines start
|
||||
* with a tabulation of four white spaces.
|
||||
*/
|
||||
private function unindentCode(string $code): string
|
||||
{
|
||||
$codeLines = u($code)->split("\n");
|
||||
|
||||
$indentedOrBlankLines = array_filter($codeLines, function ($lineOfCode) {
|
||||
return u($lineOfCode)->isEmpty() || u($lineOfCode)->startsWith(' ');
|
||||
});
|
||||
|
||||
$codeIsIndented = \count($indentedOrBlankLines) === \count($codeLines);
|
||||
if ($codeIsIndented) {
|
||||
$unindentedLines = array_map(function ($lineOfCode) {
|
||||
return u($lineOfCode)->after(' ');
|
||||
}, $codeLines);
|
||||
$code = u("\n")->join($unindentedLines);
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
48
app/src/Utils/MomentFormatConverter.php
Normal file
48
app/src/Utils/MomentFormatConverter.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?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\Utils;
|
||||
|
||||
/**
|
||||
* This class is used to convert PHP date format to moment.js format.
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class MomentFormatConverter
|
||||
{
|
||||
/**
|
||||
* This defines the mapping between PHP ICU date format (key) and moment.js date format (value)
|
||||
* For ICU formats see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
|
||||
* For Moment formats see https://momentjs.com/docs/#/displaying/format/.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $formatConvertRules = [
|
||||
// year
|
||||
'yyyy' => 'YYYY', 'yy' => 'YY', 'y' => 'YYYY',
|
||||
// day
|
||||
'dd' => 'DD', 'd' => 'D',
|
||||
// day of week
|
||||
'EE' => 'ddd', 'EEEEEE' => 'dd',
|
||||
// timezone
|
||||
'ZZZZZ' => 'Z', 'ZZZ' => 'ZZ',
|
||||
// letter 'T'
|
||||
'\'T\'' => 'T',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns associated moment.js format.
|
||||
*/
|
||||
public function convert(string $format): string
|
||||
{
|
||||
return strtr($format, self::$formatConvertRules);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user