Compare commits
3 Commits
master
...
symfony_de
Author | SHA1 | Date | |
---|---|---|---|
3751003188 | |||
72fd1cf02d | |||
f8cfbebd26 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
# Comment to include Symfony project
|
# Comment to include Symfony project
|
||||||
app
|
#app/*
|
||||||
|
!app/.keep
|
||||||
|
|
||||||
# Ignore Database datas
|
# Ignore Database datas
|
||||||
data
|
data
|
||||||
|
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
|
24
app/.gitignore
vendored
Normal file
24
app/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/public/build/fonts/glyphicons-*
|
||||||
|
/public/build/images/glyphicons-*
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
/.env.local
|
||||||
|
/.env.local.php
|
||||||
|
/.env.*.local
|
||||||
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
|
/public/bundles/
|
||||||
|
/var/
|
||||||
|
/vendor/
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/phpunit-bridge ###
|
||||||
|
.phpunit
|
||||||
|
.phpunit.result.cache
|
||||||
|
/phpunit.xml
|
||||||
|
###< symfony/phpunit-bridge ###
|
||||||
|
###> symfony/webpack-encore-bundle ###
|
||||||
|
/node_modules/
|
||||||
|
/public/build/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
###< symfony/webpack-encore-bundle ###
|
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
|
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;
|
||||||
|
}
|
43
app/bin/console
Executable file
43
app/bin/console
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Dotenv\Dotenv;
|
||||||
|
use Symfony\Component\ErrorHandler\Debug;
|
||||||
|
|
||||||
|
if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
|
||||||
|
echo 'Warning: The console should be invoked via the CLI version of PHP, not the '.PHP_SAPI.' SAPI'.PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_time_limit(0);
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (!class_exists(Application::class) || !class_exists(Dotenv::class)) {
|
||||||
|
throw new LogicException('You need to add "symfony/framework-bundle" and "symfony/dotenv" as Composer dependencies.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = new ArgvInput();
|
||||||
|
if (null !== $env = $input->getParameterOption(['--env', '-e'], null, true)) {
|
||||||
|
putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input->hasParameterOption('--no-debug', true)) {
|
||||||
|
putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||||
|
|
||||||
|
if ($_SERVER['APP_DEBUG']) {
|
||||||
|
umask(0000);
|
||||||
|
|
||||||
|
if (class_exists(Debug::class)) {
|
||||||
|
Debug::enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||||
|
$application = new Application($kernel);
|
||||||
|
$application->run($input);
|
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';
|
92
app/composer.json
Normal file
92
app/composer.json
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"license": "proprietary",
|
||||||
|
"type": "project",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"replace": {
|
||||||
|
"symfony/polyfill-php70": "*",
|
||||||
|
"symfony/polyfill-php72": "*"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2.9",
|
||||||
|
"ext-pdo_sqlite": "*",
|
||||||
|
"composer/package-versions-deprecated": "^1.8",
|
||||||
|
"doctrine/doctrine-bundle": "^1.12|^2.0",
|
||||||
|
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||||
|
"doctrine/orm": "^2.5.11",
|
||||||
|
"erusev/parsedown": "^1.6",
|
||||||
|
"sensio/framework-extra-bundle": "^5.6",
|
||||||
|
"symfony/apache-pack": "^1.0",
|
||||||
|
"symfony/asset": "^5.2",
|
||||||
|
"symfony/console": "^5.2",
|
||||||
|
"symfony/dotenv": "^5.2",
|
||||||
|
"symfony/expression-language": "^5.2",
|
||||||
|
"symfony/flex": "^1.1",
|
||||||
|
"symfony/form": "^5.2",
|
||||||
|
"symfony/framework-bundle": "^5.2",
|
||||||
|
"symfony/intl": "^5.2",
|
||||||
|
"symfony/mailer": "^5.2",
|
||||||
|
"symfony/monolog-bundle": "^3.1",
|
||||||
|
"symfony/polyfill-intl-messageformatter": "^1.12",
|
||||||
|
"symfony/security-bundle": "^5.2",
|
||||||
|
"symfony/string": "^5.2",
|
||||||
|
"symfony/translation": "^5.2",
|
||||||
|
"symfony/twig-pack": "^1.0",
|
||||||
|
"symfony/validator": "^5.2",
|
||||||
|
"symfony/webpack-encore-bundle": "^1.4",
|
||||||
|
"symfony/yaml": "^5.2",
|
||||||
|
"tgalopin/html-sanitizer-bundle": "^1.2",
|
||||||
|
"twig/intl-extra": "^3.0",
|
||||||
|
"twig/markdown-extra": "^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dama/doctrine-test-bundle": "^6.2",
|
||||||
|
"doctrine/doctrine-fixtures-bundle": "^3.0",
|
||||||
|
"symfony/browser-kit": "^5.2",
|
||||||
|
"symfony/css-selector": "^5.2",
|
||||||
|
"symfony/debug-bundle": "^5.2",
|
||||||
|
"symfony/maker-bundle": "^1.11",
|
||||||
|
"symfony/phpunit-bridge": "^5.2",
|
||||||
|
"symfony/stopwatch": "^5.2",
|
||||||
|
"symfony/web-profiler-bundle": "^5.2"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"platform": {
|
||||||
|
"php": "7.2.9"
|
||||||
|
},
|
||||||
|
"preferred-install": {
|
||||||
|
"*": "dist"
|
||||||
|
},
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"auto-scripts": {
|
||||||
|
"cache:clear": "symfony-cmd",
|
||||||
|
"assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd"
|
||||||
|
},
|
||||||
|
"post-install-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/symfony": "*"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"symfony": {
|
||||||
|
"allow-contrib": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8273
app/composer.lock
generated
Normal file
8273
app/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
app/config/bundles.php
Normal file
19
app/config/bundles.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
|
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
|
||||||
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
|
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||||
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
|
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||||
|
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
|
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||||
|
HtmlSanitizer\Bundle\HtmlSanitizerBundle::class => ['all' => true],
|
||||||
|
];
|
3
app/config/packages/assets.yaml
Normal file
3
app/config/packages/assets.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
framework:
|
||||||
|
assets:
|
||||||
|
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
19
app/config/packages/cache.yaml
Normal file
19
app/config/packages/cache.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||||
|
#prefix_seed: your_vendor_name/app_name
|
||||||
|
|
||||||
|
# The "app" cache stores to the filesystem by default.
|
||||||
|
# The data in this cache should persist between deploys.
|
||||||
|
# Other options include:
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
#app: cache.adapter.redis
|
||||||
|
#default_redis_provider: redis://localhost
|
||||||
|
|
||||||
|
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||||
|
#app: cache.adapter.apcu
|
||||||
|
|
||||||
|
# Namespaced pools use the above "app" backend by default
|
||||||
|
#pools:
|
||||||
|
#my.dedicated.cache: null
|
4
app/config/packages/dev/debug.yaml
Normal file
4
app/config/packages/dev/debug.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
debug:
|
||||||
|
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||||
|
# See the "server:dump" command to start a new server.
|
||||||
|
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
4
app/config/packages/dev/mailer.yaml
Normal file
4
app/config/packages/dev/mailer.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
framework:
|
||||||
|
mailer:
|
||||||
|
# this disables delivery of messages entirely
|
||||||
|
dsn: 'null://null'
|
19
app/config/packages/dev/monolog.yaml
Normal file
19
app/config/packages/dev/monolog.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
channels: ["!event"]
|
||||||
|
# uncomment to get logging in your browser
|
||||||
|
# you may have to allow bigger header sizes in your Web server configuration
|
||||||
|
#firephp:
|
||||||
|
# type: firephp
|
||||||
|
# level: info
|
||||||
|
#chromephp:
|
||||||
|
# type: chromephp
|
||||||
|
# level: info
|
||||||
|
console:
|
||||||
|
type: console
|
||||||
|
process_psr_3_messages: false
|
||||||
|
channels: ["!event", "!doctrine", "!console"]
|
6
app/config/packages/dev/web_profiler.yaml
Normal file
6
app/config/packages/dev/web_profiler.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
web_profiler:
|
||||||
|
toolbar: true
|
||||||
|
intercept_redirects: false
|
||||||
|
|
||||||
|
framework:
|
||||||
|
profiler: { only_exceptions: false }
|
18
app/config/packages/doctrine.yaml
Normal file
18
app/config/packages/doctrine.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
|
||||||
|
# IMPORTANT: You MUST configure your server version,
|
||||||
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
|
#server_version: '5.7'
|
||||||
|
orm:
|
||||||
|
auto_generate_proxy_classes: true
|
||||||
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
|
auto_mapping: true
|
||||||
|
mappings:
|
||||||
|
App:
|
||||||
|
is_bundle: false
|
||||||
|
type: annotation
|
||||||
|
dir: '%kernel.project_dir%/src/Entity'
|
||||||
|
prefix: 'App\Entity'
|
||||||
|
alias: App
|
5
app/config/packages/doctrine_migrations.yaml
Normal file
5
app/config/packages/doctrine_migrations.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
doctrine_migrations:
|
||||||
|
migrations_paths:
|
||||||
|
# namespace is arbitrary but should be different from App\Migrations
|
||||||
|
# as migrations classes should NOT be autoloaded
|
||||||
|
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
27
app/config/packages/framework.yaml
Normal file
27
app/config/packages/framework.yaml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
|
framework:
|
||||||
|
secret: '%env(APP_SECRET)%'
|
||||||
|
csrf_protection: true
|
||||||
|
http_method_override: true
|
||||||
|
|
||||||
|
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||||
|
# Remove or comment this section to explicitly disable session support.
|
||||||
|
session:
|
||||||
|
handler_id: null
|
||||||
|
cookie_secure: auto
|
||||||
|
cookie_samesite: lax
|
||||||
|
|
||||||
|
# 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"
|
20
app/config/packages/prod/doctrine.yaml
Normal file
20
app/config/packages/prod/doctrine.yaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
doctrine:
|
||||||
|
orm:
|
||||||
|
auto_generate_proxy_classes: false
|
||||||
|
metadata_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.system_cache_pool
|
||||||
|
query_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.system_cache_pool
|
||||||
|
result_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.result_cache_pool
|
||||||
|
|
||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
pools:
|
||||||
|
doctrine.result_cache_pool:
|
||||||
|
adapter: cache.app
|
||||||
|
doctrine.system_cache_pool:
|
||||||
|
adapter: cache.system
|
16
app/config/packages/prod/monolog.yaml
Normal file
16
app/config/packages/prod/monolog.yaml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: fingers_crossed
|
||||||
|
action_level: error
|
||||||
|
handler: nested
|
||||||
|
excluded_http_codes: [404, 405]
|
||||||
|
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||||
|
nested:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
console:
|
||||||
|
type: console
|
||||||
|
process_psr_3_messages: false
|
||||||
|
channels: ["!event", "!doctrine"]
|
3
app/config/packages/prod/routing.yaml
Normal file
3
app/config/packages/prod/routing.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
framework:
|
||||||
|
router:
|
||||||
|
strict_requirements: null
|
4
app/config/packages/prod/webpack_encore.yaml
Normal file
4
app/config/packages/prod/webpack_encore.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#webpack_encore:
|
||||||
|
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||||
|
# Available in version 1.2
|
||||||
|
#cache: true
|
7
app/config/packages/routing.yaml
Normal file
7
app/config/packages/routing.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
framework:
|
||||||
|
router:
|
||||||
|
utf8: true
|
||||||
|
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
#default_uri: http://localhost
|
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
|
3
app/config/packages/sensio_framework_extra.yaml
Normal file
3
app/config/packages/sensio_framework_extra.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
sensio_framework_extra:
|
||||||
|
router:
|
||||||
|
annotations: false
|
4
app/config/packages/test/dama_doctrine_test_bundle.yaml
Normal file
4
app/config/packages/test/dama_doctrine_test_bundle.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dama_doctrine_test:
|
||||||
|
enable_static_connection: true
|
||||||
|
enable_static_meta_data_cache: true
|
||||||
|
enable_static_query_cache: true
|
4
app/config/packages/test/framework.yaml
Normal file
4
app/config/packages/test/framework.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
framework:
|
||||||
|
test: true
|
||||||
|
session:
|
||||||
|
storage_id: session.storage.mock_file
|
4
app/config/packages/test/mailer.yaml
Normal file
4
app/config/packages/test/mailer.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
framework:
|
||||||
|
mailer:
|
||||||
|
# this disables delivery of messages entirely
|
||||||
|
dsn: 'null://null'
|
12
app/config/packages/test/monolog.yaml
Normal file
12
app/config/packages/test/monolog.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: fingers_crossed
|
||||||
|
action_level: error
|
||||||
|
handler: nested
|
||||||
|
excluded_http_codes: [404, 405]
|
||||||
|
channels: ["!event"]
|
||||||
|
nested:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
6
app/config/packages/test/security.yaml
Normal file
6
app/config/packages/test/security.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# this configuration simplifies testing URLs protected by the security mechanism
|
||||||
|
# See https://symfony.com/doc/current/testing/http_authentication.html
|
||||||
|
security:
|
||||||
|
firewalls:
|
||||||
|
main:
|
||||||
|
http_basic: ~
|
2
app/config/packages/test/twig.yaml
Normal file
2
app/config/packages/test/twig.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
twig:
|
||||||
|
strict_variables: true
|
3
app/config/packages/test/validator.yaml
Normal file
3
app/config/packages/test/validator.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
not_compromised_password: false
|
6
app/config/packages/test/web_profiler.yaml
Normal file
6
app/config/packages/test/web_profiler.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
web_profiler:
|
||||||
|
toolbar: false
|
||||||
|
intercept_redirects: false
|
||||||
|
|
||||||
|
framework:
|
||||||
|
profiler: { collect: false }
|
2
app/config/packages/test/webpack_encore.yaml
Normal file
2
app/config/packages/test/webpack_encore.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#webpack_encore:
|
||||||
|
# strict_mode: false
|
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%'
|
5
app/config/packages/twig.yaml
Normal file
5
app/config/packages/twig.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
twig:
|
||||||
|
default_path: '%kernel.project_dir%/templates'
|
||||||
|
form_themes:
|
||||||
|
- 'form/layout.html.twig'
|
||||||
|
- 'form/fields.html.twig'
|
9
app/config/packages/validator.yaml
Normal file
9
app/config/packages/validator.yaml
Normal file
@ -0,0 +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\: []
|
25
app/config/packages/webpack_encore.yaml
Normal file
25
app/config/packages/webpack_encore.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
webpack_encore:
|
||||||
|
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||||
|
output_path: '%kernel.project_dir%/public/build'
|
||||||
|
# If multiple builds are defined (as shown below), you can disable the default build:
|
||||||
|
# output_path: false
|
||||||
|
|
||||||
|
# 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 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:
|
||||||
|
# builds:
|
||||||
|
# pass "frontend" as the 3rg arg to the Twig functions
|
||||||
|
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||||
|
|
||||||
|
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||||
|
|
||||||
|
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||||
|
# Put in config/packages/prod/webpack_encore.yaml
|
||||||
|
# cache: true
|
5
app/config/preload.php
Normal file
5
app/config/preload.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||||
|
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||||
|
}
|
12
app/config/routes.yaml
Normal file
12
app/config/routes.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# 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%'
|
8
app/config/routes/annotations.yaml
Normal file
8
app/config/routes/annotations.yaml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
controllers:
|
||||||
|
resource: '../../src/Controller/'
|
||||||
|
type: annotation
|
||||||
|
prefix: /{_locale}
|
||||||
|
requirements:
|
||||||
|
_locale: '%app_locales%'
|
||||||
|
defaults:
|
||||||
|
_locale: '%locale%'
|
3
app/config/routes/dev/framework.yaml
Normal file
3
app/config/routes/dev/framework.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
_errors:
|
||||||
|
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||||
|
prefix: /_error
|
7
app/config/routes/dev/web_profiler.yaml
Normal file
7
app/config/routes/dev/web_profiler.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
web_profiler_wdt:
|
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||||
|
prefix: /_wdt
|
||||||
|
|
||||||
|
web_profiler_profiler:
|
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||||
|
prefix: /_profiler
|
41
app/config/services.yaml
Normal file
41
app/config/services.yaml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# This file is the entry point to configure your own services.
|
||||||
|
# Files in the packages/ subdirectory configure your dependencies.
|
||||||
|
|
||||||
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
|
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||||
|
parameters:
|
||||||
|
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
|
||||||
|
App\:
|
||||||
|
resource: '../src/'
|
||||||
|
exclude:
|
||||||
|
- '../src/DependencyInjection/'
|
||||||
|
- '../src/Entity/'
|
||||||
|
- '../src/Kernel.php'
|
||||||
|
- '../src/Tests/'
|
||||||
|
|
||||||
|
# controllers are imported separately to make sure services can be injected
|
||||||
|
# as action arguments even if you don't extend any base controller class
|
||||||
|
App\Controller\:
|
||||||
|
resource: '../src/Controller/'
|
||||||
|
tags: ['controller.service_arguments']
|
||||||
|
|
||||||
|
# when the service definition only contains arguments, you can omit the
|
||||||
|
# 'arguments' key and define the arguments just below the service class
|
||||||
|
App\EventSubscriber\CommentNotificationSubscriber:
|
||||||
|
$sender: '%app.notifications.email_sender%'
|
0
app/migrations/.gitignore
vendored
Normal file
0
app/migrations/.gitignore
vendored
Normal file
27
app/package.json
Normal file
27
app/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^5.8.1",
|
||||||
|
"@symfony/webpack-encore": "^0.31.0",
|
||||||
|
"bloodhound-js": "^1.2.3",
|
||||||
|
"bootstrap-sass": "^3.3.7",
|
||||||
|
"bootstrap-tagsinput": "^0.7.1",
|
||||||
|
"bootswatch": "^3.3.7",
|
||||||
|
"core-js": "^3.0.0",
|
||||||
|
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
|
||||||
|
"highlight.js": "^10.4.1",
|
||||||
|
"imports-loader": "^0.8.0",
|
||||||
|
"jquery": "^3.5.1",
|
||||||
|
"lato-font": "^3.0.0",
|
||||||
|
"node-sass": "^4.9.3",
|
||||||
|
"sass-loader": "^9.0.1",
|
||||||
|
"typeahead.js": "^0.11.1"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev-server": "encore dev-server",
|
||||||
|
"dev": "encore dev",
|
||||||
|
"watch": "encore dev --watch",
|
||||||
|
"build": "encore production --progress"
|
||||||
|
}
|
||||||
|
}
|
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 |
30
app/public/index.php
Normal file
30
app/public/index.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
use Symfony\Component\Dotenv\Dotenv;
|
||||||
|
use Symfony\Component\ErrorHandler\Debug;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/autoload.php';
|
||||||
|
|
||||||
|
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||||
|
|
||||||
|
if ($_SERVER['APP_DEBUG']) {
|
||||||
|
umask(0000);
|
||||||
|
|
||||||
|
Debug::enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) {
|
||||||
|
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) {
|
||||||
|
Request::setTrustedHosts([$trustedHosts]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||||
|
$request = Request::createFromGlobals();
|
||||||
|
$response = $kernel->handle($request);
|
||||||
|
$response->send();
|
||||||
|
$kernel->terminate($request, $response);
|
4
app/public/robots.txt
Normal file
4
app/public/robots.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# www.robotstxt.org/
|
||||||
|
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
|
||||||
|
|
||||||
|
User-agent: *
|
263
app/src/Command/AddUserCommand.php
Normal file
263
app/src/Command/AddUserCommand.php
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use App\Utils\Validator;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Exception\RuntimeException;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||||
|
use Symfony\Component\Stopwatch\Stopwatch;
|
||||||
|
use function Symfony\Component\String\u;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A console command that creates users and stores them in the database.
|
||||||
|
*
|
||||||
|
* To use this command, open a terminal window, enter into your project
|
||||||
|
* directory and execute the following:
|
||||||
|
*
|
||||||
|
* $ php bin/console app:add-user
|
||||||
|
*
|
||||||
|
* To output detailed information, increase the command verbosity:
|
||||||
|
*
|
||||||
|
* $ php bin/console app:add-user -vv
|
||||||
|
*
|
||||||
|
* See https://symfony.com/doc/current/console.html
|
||||||
|
*
|
||||||
|
* We use the default services.yaml configuration, so command classes are registered as services.
|
||||||
|
* See https://symfony.com/doc/current/console/commands_as_services.html
|
||||||
|
*
|
||||||
|
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||||
|
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||||
|
*/
|
||||||
|
class AddUserCommand extends Command
|
||||||
|
{
|
||||||
|
// to make your command lazily loaded, configure the $defaultName static property,
|
||||||
|
// so it will be instantiated only when the command is actually called.
|
||||||
|
protected static $defaultName = 'app:add-user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var SymfonyStyle
|
||||||
|
*/
|
||||||
|
private $io;
|
||||||
|
|
||||||
|
private $entityManager;
|
||||||
|
private $passwordEncoder;
|
||||||
|
private $validator;
|
||||||
|
private $users;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $em, UserPasswordEncoderInterface $encoder, Validator $validator, UserRepository $users)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->entityManager = $em;
|
||||||
|
$this->passwordEncoder = $encoder;
|
||||||
|
$this->validator = $validator;
|
||||||
|
$this->users = $users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('Creates users and stores them in the database')
|
||||||
|
->setHelp($this->getCommandHelp())
|
||||||
|
// commands can optionally define arguments and/or options (mandatory and optional)
|
||||||
|
// see https://symfony.com/doc/current/components/console/console_arguments.html
|
||||||
|
->addArgument('username', InputArgument::OPTIONAL, 'The username of the new user')
|
||||||
|
->addArgument('password', InputArgument::OPTIONAL, 'The plain password of the new user')
|
||||||
|
->addArgument('email', InputArgument::OPTIONAL, 'The email of the new user')
|
||||||
|
->addArgument('full-name', InputArgument::OPTIONAL, 'The full name of the new user')
|
||||||
|
->addOption('admin', null, InputOption::VALUE_NONE, 'If set, the user is created as an administrator')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This optional method is the first one executed for a command after configure()
|
||||||
|
* and is useful to initialize properties based on the input arguments and options.
|
||||||
|
*/
|
||||||
|
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||||
|
{
|
||||||
|
// SymfonyStyle is an optional feature that Symfony provides so you can
|
||||||
|
// apply a consistent look to the commands of your application.
|
||||||
|
// See https://symfony.com/doc/current/console/style.html
|
||||||
|
$this->io = new SymfonyStyle($input, $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is executed after initialize() and before execute(). Its purpose
|
||||||
|
* is to check if some of the options/arguments are missing and interactively
|
||||||
|
* ask the user for those values.
|
||||||
|
*
|
||||||
|
* This method is completely optional. If you are developing an internal console
|
||||||
|
* command, you probably should not implement this method because it requires
|
||||||
|
* quite a lot of work. However, if the command is meant to be used by external
|
||||||
|
* users, this method is a nice way to fall back and prevent errors.
|
||||||
|
*/
|
||||||
|
protected function interact(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
if (null !== $input->getArgument('username') && null !== $input->getArgument('password') && null !== $input->getArgument('email') && null !== $input->getArgument('full-name')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->io->title('Add User Command Interactive Wizard');
|
||||||
|
$this->io->text([
|
||||||
|
'If you prefer to not use this interactive wizard, provide the',
|
||||||
|
'arguments required by this command as follows:',
|
||||||
|
'',
|
||||||
|
' $ php bin/console app:add-user username password email@example.com',
|
||||||
|
'',
|
||||||
|
'Now we\'ll ask you for the value of all the missing command arguments.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Ask for the username if it's not defined
|
||||||
|
$username = $input->getArgument('username');
|
||||||
|
if (null !== $username) {
|
||||||
|
$this->io->text(' > <info>Username</info>: '.$username);
|
||||||
|
} else {
|
||||||
|
$username = $this->io->ask('Username', null, [$this->validator, 'validateUsername']);
|
||||||
|
$input->setArgument('username', $username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for the password if it's not defined
|
||||||
|
$password = $input->getArgument('password');
|
||||||
|
if (null !== $password) {
|
||||||
|
$this->io->text(' > <info>Password</info>: '.u('*')->repeat(u($password)->length()));
|
||||||
|
} else {
|
||||||
|
$password = $this->io->askHidden('Password (your type will be hidden)', [$this->validator, 'validatePassword']);
|
||||||
|
$input->setArgument('password', $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for the email if it's not defined
|
||||||
|
$email = $input->getArgument('email');
|
||||||
|
if (null !== $email) {
|
||||||
|
$this->io->text(' > <info>Email</info>: '.$email);
|
||||||
|
} else {
|
||||||
|
$email = $this->io->ask('Email', null, [$this->validator, 'validateEmail']);
|
||||||
|
$input->setArgument('email', $email);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for the full name if it's not defined
|
||||||
|
$fullName = $input->getArgument('full-name');
|
||||||
|
if (null !== $fullName) {
|
||||||
|
$this->io->text(' > <info>Full Name</info>: '.$fullName);
|
||||||
|
} else {
|
||||||
|
$fullName = $this->io->ask('Full Name', null, [$this->validator, 'validateFullName']);
|
||||||
|
$input->setArgument('full-name', $fullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is executed after interact() and initialize(). It usually
|
||||||
|
* contains the logic to execute to complete this command task.
|
||||||
|
*/
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$stopwatch = new Stopwatch();
|
||||||
|
$stopwatch->start('add-user-command');
|
||||||
|
|
||||||
|
$username = $input->getArgument('username');
|
||||||
|
$plainPassword = $input->getArgument('password');
|
||||||
|
$email = $input->getArgument('email');
|
||||||
|
$fullName = $input->getArgument('full-name');
|
||||||
|
$isAdmin = $input->getOption('admin');
|
||||||
|
|
||||||
|
// make sure to validate the user data is correct
|
||||||
|
$this->validateUserData($username, $plainPassword, $email, $fullName);
|
||||||
|
|
||||||
|
// create the user and encode its password
|
||||||
|
$user = new User();
|
||||||
|
$user->setFullName($fullName);
|
||||||
|
$user->setUsername($username);
|
||||||
|
$user->setEmail($email);
|
||||||
|
$user->setRoles([$isAdmin ? 'ROLE_ADMIN' : 'ROLE_USER']);
|
||||||
|
|
||||||
|
// See https://symfony.com/doc/current/security.html#c-encoding-passwords
|
||||||
|
$encodedPassword = $this->passwordEncoder->encodePassword($user, $plainPassword);
|
||||||
|
$user->setPassword($encodedPassword);
|
||||||
|
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->io->success(sprintf('%s was successfully created: %s (%s)', $isAdmin ? 'Administrator user' : 'User', $user->getUsername(), $user->getEmail()));
|
||||||
|
|
||||||
|
$event = $stopwatch->stop('add-user-command');
|
||||||
|
if ($output->isVerbose()) {
|
||||||
|
$this->io->comment(sprintf('New user database id: %d / Elapsed time: %.2f ms / Consumed memory: %.2f MB', $user->getId(), $event->getDuration(), $event->getMemory() / (1024 ** 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateUserData($username, $plainPassword, $email, $fullName): void
|
||||||
|
{
|
||||||
|
// first check if a user with the same username already exists.
|
||||||
|
$existingUser = $this->users->findOneBy(['username' => $username]);
|
||||||
|
|
||||||
|
if (null !== $existingUser) {
|
||||||
|
throw new RuntimeException(sprintf('There is already a user registered with the "%s" username.', $username));
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate password and email if is not this input means interactive.
|
||||||
|
$this->validator->validatePassword($plainPassword);
|
||||||
|
$this->validator->validateEmail($email);
|
||||||
|
$this->validator->validateFullName($fullName);
|
||||||
|
|
||||||
|
// check if a user with the same email already exists.
|
||||||
|
$existingEmail = $this->users->findOneBy(['email' => $email]);
|
||||||
|
|
||||||
|
if (null !== $existingEmail) {
|
||||||
|
throw new RuntimeException(sprintf('There is already a user registered with the "%s" email.', $email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The command help is usually included in the configure() method, but when
|
||||||
|
* it's too long, it's better to define a separate method to maintain the
|
||||||
|
* code readability.
|
||||||
|
*/
|
||||||
|
private function getCommandHelp(): string
|
||||||
|
{
|
||||||
|
return <<<'HELP'
|
||||||
|
The <info>%command.name%</info> command creates new users and saves them in the database:
|
||||||
|
|
||||||
|
<info>php %command.full_name%</info> <comment>username password email</comment>
|
||||||
|
|
||||||
|
By default the command creates regular users. To create administrator users,
|
||||||
|
add the <comment>--admin</comment> option:
|
||||||
|
|
||||||
|
<info>php %command.full_name%</info> username password email <comment>--admin</comment>
|
||||||
|
|
||||||
|
If you omit any of the three required arguments, the command will ask you to
|
||||||
|
provide the missing values:
|
||||||
|
|
||||||
|
# command will ask you for the email
|
||||||
|
<info>php %command.full_name%</info> <comment>username password</comment>
|
||||||
|
|
||||||
|
# command will ask you for the email and password
|
||||||
|
<info>php %command.full_name%</info> <comment>username</comment>
|
||||||
|
|
||||||
|
# command will ask you for all arguments
|
||||||
|
<info>php %command.full_name%</info>
|
||||||
|
|
||||||
|
HELP;
|
||||||
|
}
|
||||||
|
}
|
132
app/src/Command/DeleteUserCommand.php
Normal file
132
app/src/Command/DeleteUserCommand.php
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use App\Utils\Validator;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Exception\RuntimeException;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A console command that deletes users from the database.
|
||||||
|
*
|
||||||
|
* To use this command, open a terminal window, enter into your project
|
||||||
|
* directory and execute the following:
|
||||||
|
*
|
||||||
|
* $ php bin/console app:delete-user
|
||||||
|
*
|
||||||
|
* Check out the code of the src/Command/AddUserCommand.php file for
|
||||||
|
* the full explanation about Symfony commands.
|
||||||
|
*
|
||||||
|
* See https://symfony.com/doc/current/console.html
|
||||||
|
*
|
||||||
|
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
|
||||||
|
*/
|
||||||
|
class DeleteUserCommand extends Command
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'app:delete-user';
|
||||||
|
|
||||||
|
/** @var SymfonyStyle */
|
||||||
|
private $io;
|
||||||
|
private $entityManager;
|
||||||
|
private $validator;
|
||||||
|
private $users;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $em, Validator $validator, UserRepository $users)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->entityManager = $em;
|
||||||
|
$this->validator = $validator;
|
||||||
|
$this->users = $users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('Deletes users from the database')
|
||||||
|
->addArgument('username', InputArgument::REQUIRED, 'The username of an existing user')
|
||||||
|
->setHelp(<<<'HELP'
|
||||||
|
The <info>%command.name%</info> command deletes users from the database:
|
||||||
|
|
||||||
|
<info>php %command.full_name%</info> <comment>username</comment>
|
||||||
|
|
||||||
|
If you omit the argument, the command will ask you to
|
||||||
|
provide the missing value:
|
||||||
|
|
||||||
|
<info>php %command.full_name%</info>
|
||||||
|
HELP
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||||
|
{
|
||||||
|
// SymfonyStyle is an optional feature that Symfony provides so you can
|
||||||
|
// apply a consistent look to the commands of your application.
|
||||||
|
// See https://symfony.com/doc/current/console/style.html
|
||||||
|
$this->io = new SymfonyStyle($input, $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function interact(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
if (null !== $input->getArgument('username')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->io->title('Delete User Command Interactive Wizard');
|
||||||
|
$this->io->text([
|
||||||
|
'If you prefer to not use this interactive wizard, provide the',
|
||||||
|
'arguments required by this command as follows:',
|
||||||
|
'',
|
||||||
|
' $ php bin/console app:delete-user username',
|
||||||
|
'',
|
||||||
|
'Now we\'ll ask you for the value of all the missing command arguments.',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$username = $this->io->ask('Username', null, [$this->validator, 'validateUsername']);
|
||||||
|
$input->setArgument('username', $username);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$username = $this->validator->validateUsername($input->getArgument('username'));
|
||||||
|
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $this->users->findOneByUsername($username);
|
||||||
|
|
||||||
|
if (null === $user) {
|
||||||
|
throw new RuntimeException(sprintf('User with username "%s" not found.', $username));
|
||||||
|
}
|
||||||
|
|
||||||
|
// After an entity has been removed its in-memory state is the same
|
||||||
|
// as before the removal, except for generated identifiers.
|
||||||
|
// See https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#removing-entities
|
||||||
|
$userId = $user->getId();
|
||||||
|
|
||||||
|
$this->entityManager->remove($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->io->success(sprintf('User "%s" (ID: %d, email: %s) was successfully deleted.', $user->getUsername(), $userId, $user->getEmail()));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
146
app/src/Command/ListUsersCommand.php
Normal file
146
app/src/Command/ListUsersCommand.php
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A console command that lists all the existing users.
|
||||||
|
*
|
||||||
|
* To use this command, open a terminal window, enter into your project directory
|
||||||
|
* and execute the following:
|
||||||
|
*
|
||||||
|
* $ php bin/console app:list-users
|
||||||
|
*
|
||||||
|
* Check out the code of the src/Command/AddUserCommand.php file for
|
||||||
|
* the full explanation about Symfony commands.
|
||||||
|
*
|
||||||
|
* See https://symfony.com/doc/current/console.html
|
||||||
|
*
|
||||||
|
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||||
|
*/
|
||||||
|
class ListUsersCommand extends Command
|
||||||
|
{
|
||||||
|
// a good practice is to use the 'app:' prefix to group all your custom application commands
|
||||||
|
protected static $defaultName = 'app:list-users';
|
||||||
|
|
||||||
|
private $mailer;
|
||||||
|
private $emailSender;
|
||||||
|
private $users;
|
||||||
|
|
||||||
|
public function __construct(MailerInterface $mailer, string $emailSender, UserRepository $users)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->mailer = $mailer;
|
||||||
|
$this->emailSender = $emailSender;
|
||||||
|
$this->users = $users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('Lists all the existing users')
|
||||||
|
->setHelp(<<<'HELP'
|
||||||
|
The <info>%command.name%</info> command lists all the users registered in the application:
|
||||||
|
|
||||||
|
<info>php %command.full_name%</info>
|
||||||
|
|
||||||
|
By default the command only displays the 50 most recent users. Set the number of
|
||||||
|
results to display with the <comment>--max-results</comment> option:
|
||||||
|
|
||||||
|
<info>php %command.full_name%</info> <comment>--max-results=2000</comment>
|
||||||
|
|
||||||
|
In addition to displaying the user list, you can also send this information to
|
||||||
|
the email address specified in the <comment>--send-to</comment> option:
|
||||||
|
|
||||||
|
<info>php %command.full_name%</info> <comment>--send-to=fabien@symfony.com</comment>
|
||||||
|
|
||||||
|
HELP
|
||||||
|
)
|
||||||
|
// commands can optionally define arguments and/or options (mandatory and optional)
|
||||||
|
// see https://symfony.com/doc/current/components/console/console_arguments.html
|
||||||
|
->addOption('max-results', null, InputOption::VALUE_OPTIONAL, 'Limits the number of users listed', 50)
|
||||||
|
->addOption('send-to', null, InputOption::VALUE_OPTIONAL, 'If set, the result is sent to the given email address')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is executed after initialize(). It usually contains the logic
|
||||||
|
* to execute to complete this command task.
|
||||||
|
*/
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$maxResults = $input->getOption('max-results');
|
||||||
|
// Use ->findBy() instead of ->findAll() to allow result sorting and limiting
|
||||||
|
$allUsers = $this->users->findBy([], ['id' => 'DESC'], $maxResults);
|
||||||
|
|
||||||
|
// Doctrine query returns an array of objects and we need an array of plain arrays
|
||||||
|
$usersAsPlainArrays = array_map(function (User $user) {
|
||||||
|
return [
|
||||||
|
$user->getId(),
|
||||||
|
$user->getFullName(),
|
||||||
|
$user->getUsername(),
|
||||||
|
$user->getEmail(),
|
||||||
|
implode(', ', $user->getRoles()),
|
||||||
|
];
|
||||||
|
}, $allUsers);
|
||||||
|
|
||||||
|
// In your console commands you should always use the regular output type,
|
||||||
|
// which outputs contents directly in the console window. However, this
|
||||||
|
// command uses the BufferedOutput type instead, to be able to get the output
|
||||||
|
// contents before displaying them. This is needed because the command allows
|
||||||
|
// to send the list of users via email with the '--send-to' option
|
||||||
|
$bufferedOutput = new BufferedOutput();
|
||||||
|
$io = new SymfonyStyle($input, $bufferedOutput);
|
||||||
|
$io->table(
|
||||||
|
['ID', 'Full Name', 'Username', 'Email', 'Roles'],
|
||||||
|
$usersAsPlainArrays
|
||||||
|
);
|
||||||
|
|
||||||
|
// instead of just displaying the table of users, store its contents in a variable
|
||||||
|
$usersAsATable = $bufferedOutput->fetch();
|
||||||
|
$output->write($usersAsATable);
|
||||||
|
|
||||||
|
if (null !== $email = $input->getOption('send-to')) {
|
||||||
|
$this->sendReport($usersAsATable, $email);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the given $contents to the $recipient email address.
|
||||||
|
*/
|
||||||
|
private function sendReport(string $contents, string $recipient): void
|
||||||
|
{
|
||||||
|
$email = (new Email())
|
||||||
|
->from($this->emailSender)
|
||||||
|
->to($recipient)
|
||||||
|
->subject(sprintf('app:list-users report (%s)', date('Y-m-d H:i:s')))
|
||||||
|
->text($contents);
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
}
|
||||||
|
}
|
177
app/src/Controller/Admin/BlogController.php
Normal file
177
app/src/Controller/Admin/BlogController.php
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Entity\Post;
|
||||||
|
use App\Form\PostType;
|
||||||
|
use App\Repository\PostRepository;
|
||||||
|
use App\Security\PostVoter;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used to manage blog contents in the backend.
|
||||||
|
*
|
||||||
|
* Please note that the application backend is developed manually for learning
|
||||||
|
* purposes. However, in your real Symfony application you should use any of the
|
||||||
|
* existing bundles that let you generate ready-to-use backends without effort.
|
||||||
|
*
|
||||||
|
* See http://knpbundles.com/keyword/admin
|
||||||
|
*
|
||||||
|
* @Route("/admin/post")
|
||||||
|
* @IsGranted("ROLE_ADMIN")
|
||||||
|
*
|
||||||
|
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||||
|
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||||
|
*/
|
||||||
|
class BlogController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Lists all Post entities.
|
||||||
|
*
|
||||||
|
* This controller responds to two different routes with the same URL:
|
||||||
|
* * 'admin_post_index' is the route with a name that follows the same
|
||||||
|
* structure as the rest of the controllers of this class.
|
||||||
|
* * 'admin_index' is a nice shortcut to the backend homepage. This allows
|
||||||
|
* to create simpler links in the templates. Moreover, in the future we
|
||||||
|
* could move this annotation to any other controller while maintaining
|
||||||
|
* the route name and therefore, without breaking any existing link.
|
||||||
|
*
|
||||||
|
* @Route("/", methods="GET", name="admin_index")
|
||||||
|
* @Route("/", methods="GET", name="admin_post_index")
|
||||||
|
*/
|
||||||
|
public function index(PostRepository $posts): Response
|
||||||
|
{
|
||||||
|
$authorPosts = $posts->findBy(['author' => $this->getUser()], ['publishedAt' => 'DESC']);
|
||||||
|
|
||||||
|
return $this->render('admin/blog/index.html.twig', ['posts' => $authorPosts]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Post entity.
|
||||||
|
*
|
||||||
|
* @Route("/new", methods="GET|POST", name="admin_post_new")
|
||||||
|
*
|
||||||
|
* NOTE: the Method annotation is optional, but it's a recommended practice
|
||||||
|
* to constraint the HTTP methods each controller responds to (by default
|
||||||
|
* it responds to all methods).
|
||||||
|
*/
|
||||||
|
public function new(Request $request): Response
|
||||||
|
{
|
||||||
|
$post = new Post();
|
||||||
|
$post->setAuthor($this->getUser());
|
||||||
|
|
||||||
|
// See https://symfony.com/doc/current/form/multiple_buttons.html
|
||||||
|
$form = $this->createForm(PostType::class, $post)
|
||||||
|
->add('saveAndCreateNew', SubmitType::class);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
// the isSubmitted() method is completely optional because the other
|
||||||
|
// isValid() method already checks whether the form is submitted.
|
||||||
|
// However, we explicitly add it to improve code readability.
|
||||||
|
// See https://symfony.com/doc/current/forms.html#processing-forms
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$em = $this->getDoctrine()->getManager();
|
||||||
|
$em->persist($post);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
// Flash messages are used to notify the user about the result of the
|
||||||
|
// actions. They are deleted automatically from the session as soon
|
||||||
|
// as they are accessed.
|
||||||
|
// See https://symfony.com/doc/current/controller.html#flash-messages
|
||||||
|
$this->addFlash('success', 'post.created_successfully');
|
||||||
|
|
||||||
|
if ($form->get('saveAndCreateNew')->isClicked()) {
|
||||||
|
return $this->redirectToRoute('admin_post_new');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_post_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/blog/new.html.twig', [
|
||||||
|
'post' => $post,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds and displays a Post entity.
|
||||||
|
*
|
||||||
|
* @Route("/{id<\d+>}", methods="GET", name="admin_post_show")
|
||||||
|
*/
|
||||||
|
public function show(Post $post): Response
|
||||||
|
{
|
||||||
|
// This security check can also be performed
|
||||||
|
// using an annotation: @IsGranted("show", subject="post", message="Posts can only be shown to their authors.")
|
||||||
|
$this->denyAccessUnlessGranted(PostVoter::SHOW, $post, 'Posts can only be shown to their authors.');
|
||||||
|
|
||||||
|
return $this->render('admin/blog/show.html.twig', [
|
||||||
|
'post' => $post,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a form to edit an existing Post entity.
|
||||||
|
*
|
||||||
|
* @Route("/{id<\d+>}/edit", methods="GET|POST", name="admin_post_edit")
|
||||||
|
* @IsGranted("edit", subject="post", message="Posts can only be edited by their authors.")
|
||||||
|
*/
|
||||||
|
public function edit(Request $request, Post $post): Response
|
||||||
|
{
|
||||||
|
$form = $this->createForm(PostType::class, $post);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->getDoctrine()->getManager()->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'post.updated_successfully');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_post_edit', ['id' => $post->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/blog/edit.html.twig', [
|
||||||
|
'post' => $post,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a Post entity.
|
||||||
|
*
|
||||||
|
* @Route("/{id}/delete", methods="POST", name="admin_post_delete")
|
||||||
|
* @IsGranted("delete", subject="post")
|
||||||
|
*/
|
||||||
|
public function delete(Request $request, Post $post): Response
|
||||||
|
{
|
||||||
|
if (!$this->isCsrfTokenValid('delete', $request->request->get('token'))) {
|
||||||
|
return $this->redirectToRoute('admin_post_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the tags associated with this blog post. This is done automatically
|
||||||
|
// by Doctrine, except for SQLite (the database used in this application)
|
||||||
|
// because foreign key support is not enabled by default in SQLite
|
||||||
|
$post->getTags()->clear();
|
||||||
|
|
||||||
|
$em = $this->getDoctrine()->getManager();
|
||||||
|
$em->remove($post);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'post.deleted_successfully');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_post_index');
|
||||||
|
}
|
||||||
|
}
|
170
app/src/Controller/BlogController.php
Normal file
170
app/src/Controller/BlogController.php
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Comment;
|
||||||
|
use App\Entity\Post;
|
||||||
|
use App\Event\CommentCreatedEvent;
|
||||||
|
use App\Form\CommentType;
|
||||||
|
use App\Repository\PostRepository;
|
||||||
|
use App\Repository\TagRepository;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used to manage blog contents in the public part of the site.
|
||||||
|
*
|
||||||
|
* @Route("/blog")
|
||||||
|
*
|
||||||
|
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||||
|
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||||
|
*/
|
||||||
|
class BlogController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/", defaults={"page": "1", "_format"="html"}, methods="GET", name="blog_index")
|
||||||
|
* @Route("/rss.xml", defaults={"page": "1", "_format"="xml"}, methods="GET", name="blog_rss")
|
||||||
|
* @Route("/page/{page<[1-9]\d*>}", defaults={"_format"="html"}, methods="GET", name="blog_index_paginated")
|
||||||
|
* @Cache(smaxage="10")
|
||||||
|
*
|
||||||
|
* NOTE: For standard formats, Symfony will also automatically choose the best
|
||||||
|
* Content-Type header for the response.
|
||||||
|
* See https://symfony.com/doc/current/routing.html#special-parameters
|
||||||
|
*/
|
||||||
|
public function index(Request $request, int $page, string $_format, PostRepository $posts, TagRepository $tags): Response
|
||||||
|
{
|
||||||
|
$tag = null;
|
||||||
|
if ($request->query->has('tag')) {
|
||||||
|
$tag = $tags->findOneBy(['name' => $request->query->get('tag')]);
|
||||||
|
}
|
||||||
|
$latestPosts = $posts->findLatest($page, $tag);
|
||||||
|
|
||||||
|
// Every template name also has two extensions that specify the format and
|
||||||
|
// engine for that template.
|
||||||
|
// See https://symfony.com/doc/current/templates.html#template-naming
|
||||||
|
return $this->render('blog/index.'.$_format.'.twig', [
|
||||||
|
'paginator' => $latestPosts,
|
||||||
|
'tagName' => $tag ? $tag->getName() : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/posts/{slug}", methods="GET", name="blog_post")
|
||||||
|
*
|
||||||
|
* NOTE: The $post controller argument is automatically injected by Symfony
|
||||||
|
* after performing a database query looking for a Post with the 'slug'
|
||||||
|
* value given in the route.
|
||||||
|
* See https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
|
||||||
|
*/
|
||||||
|
public function postShow(Post $post): Response
|
||||||
|
{
|
||||||
|
// Symfony's 'dump()' function is an improved version of PHP's 'var_dump()' but
|
||||||
|
// it's not available in the 'prod' environment to prevent leaking sensitive information.
|
||||||
|
// It can be used both in PHP files and Twig templates, but it requires to
|
||||||
|
// have enabled the DebugBundle. Uncomment the following line to see it in action:
|
||||||
|
//
|
||||||
|
// dump($post, $this->getUser(), new \DateTime());
|
||||||
|
|
||||||
|
return $this->render('blog/post_show.html.twig', ['post' => $post]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/comment/{postSlug}/new", methods="POST", name="comment_new")
|
||||||
|
* @IsGranted("IS_AUTHENTICATED_FULLY")
|
||||||
|
* @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
|
||||||
|
*
|
||||||
|
* NOTE: The ParamConverter mapping is required because the route parameter
|
||||||
|
* (postSlug) doesn't match any of the Doctrine entity properties (slug).
|
||||||
|
* See https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html#doctrine-converter
|
||||||
|
*/
|
||||||
|
public function commentNew(Request $request, Post $post, EventDispatcherInterface $eventDispatcher): Response
|
||||||
|
{
|
||||||
|
$comment = new Comment();
|
||||||
|
$comment->setAuthor($this->getUser());
|
||||||
|
$post->addComment($comment);
|
||||||
|
|
||||||
|
$form = $this->createForm(CommentType::class, $comment);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$em = $this->getDoctrine()->getManager();
|
||||||
|
$em->persist($comment);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
// When an event is dispatched, Symfony notifies it to all the listeners
|
||||||
|
// and subscribers registered to it. Listeners can modify the information
|
||||||
|
// passed in the event and they can even modify the execution flow, so
|
||||||
|
// there's no guarantee that the rest of this controller will be executed.
|
||||||
|
// See https://symfony.com/doc/current/components/event_dispatcher.html
|
||||||
|
$eventDispatcher->dispatch(new CommentCreatedEvent($comment));
|
||||||
|
|
||||||
|
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('blog/comment_form_error.html.twig', [
|
||||||
|
'post' => $post,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This controller is called directly via the render() function in the
|
||||||
|
* blog/post_show.html.twig template. That's why it's not needed to define
|
||||||
|
* a route name for it.
|
||||||
|
*
|
||||||
|
* The "id" of the Post is passed in and then turned into a Post object
|
||||||
|
* automatically by the ParamConverter.
|
||||||
|
*/
|
||||||
|
public function commentForm(Post $post): Response
|
||||||
|
{
|
||||||
|
$form = $this->createForm(CommentType::class);
|
||||||
|
|
||||||
|
return $this->render('blog/_comment_form.html.twig', [
|
||||||
|
'post' => $post,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/search", methods="GET", name="blog_search")
|
||||||
|
*/
|
||||||
|
public function search(Request $request, PostRepository $posts): Response
|
||||||
|
{
|
||||||
|
$query = $request->query->get('q', '');
|
||||||
|
$limit = $request->query->get('l', 10);
|
||||||
|
|
||||||
|
if (!$request->isXmlHttpRequest()) {
|
||||||
|
return $this->render('blog/search.html.twig', ['query' => $query]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$foundPosts = $posts->findBySearchQuery($query, $limit);
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
foreach ($foundPosts as $post) {
|
||||||
|
$results[] = [
|
||||||
|
'title' => htmlspecialchars($post->getTitle(), ENT_COMPAT | ENT_HTML5),
|
||||||
|
'date' => $post->getPublishedAt()->format('M d, Y'),
|
||||||
|
'author' => htmlspecialchars($post->getAuthor()->getFullName(), ENT_COMPAT | ENT_HTML5),
|
||||||
|
'summary' => htmlspecialchars($post->getSummary(), ENT_COMPAT | ENT_HTML5),
|
||||||
|
'url' => $this->generateUrl('blog_post', ['slug' => $post->getSlug()]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json($results);
|
||||||
|
}
|
||||||
|
}
|
69
app/src/Controller/SecurityController.php
Normal file
69
app/src/Controller/SecurityController.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used to manage the application security.
|
||||||
|
* See https://symfony.com/doc/current/security/form_login_setup.html.
|
||||||
|
*
|
||||||
|
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||||
|
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||||
|
*/
|
||||||
|
class SecurityController extends AbstractController
|
||||||
|
{
|
||||||
|
use TargetPathTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/login", name="security_login")
|
||||||
|
*/
|
||||||
|
public function login(Request $request, Security $security, AuthenticationUtils $helper): Response
|
||||||
|
{
|
||||||
|
// if user is already logged in, don't display the login page again
|
||||||
|
if ($security->isGranted('ROLE_USER')) {
|
||||||
|
return $this->redirectToRoute('blog_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
// this statement solves an edge-case: if you change the locale in the login
|
||||||
|
// page, after a successful login you are redirected to a page in the previous
|
||||||
|
// locale. This code regenerates the referrer URL whenever the login page is
|
||||||
|
// browsed, to ensure that its locale is always the current one.
|
||||||
|
$this->saveTargetPath($request->getSession(), 'main', $this->generateUrl('admin_index'));
|
||||||
|
|
||||||
|
return $this->render('security/login.html.twig', [
|
||||||
|
// last username entered by the user (if any)
|
||||||
|
'last_username' => $helper->getLastUsername(),
|
||||||
|
// last authentication error (if any)
|
||||||
|
'error' => $helper->getLastAuthenticationError(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the route the user can use to logout.
|
||||||
|
*
|
||||||
|
* But, this will never be executed. Symfony will intercept this first
|
||||||
|
* and handle the logout automatically. See logout in config/packages/security.yaml
|
||||||
|
*
|
||||||
|
* @Route("/logout", name="security_logout")
|
||||||
|
*/
|
||||||
|
public function logout(): void
|
||||||
|
{
|
||||||
|
throw new \Exception('This should never be reached!');
|
||||||
|
}
|
||||||
|
}
|
79
app/src/Controller/UserController.php
Normal file
79
app/src/Controller/UserController.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Form\Type\ChangePasswordType;
|
||||||
|
use App\Form\UserType;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used to manage current user.
|
||||||
|
*
|
||||||
|
* @Route("/profile")
|
||||||
|
* @IsGranted("ROLE_USER")
|
||||||
|
*
|
||||||
|
* @author Romain Monteil <monteil.romain@gmail.com>
|
||||||
|
*/
|
||||||
|
class UserController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/edit", methods="GET|POST", name="user_edit")
|
||||||
|
*/
|
||||||
|
public function edit(Request $request): Response
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
$form = $this->createForm(UserType::class, $user);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->getDoctrine()->getManager()->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'user.updated_successfully');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('user_edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('user/edit.html.twig', [
|
||||||
|
'user' => $user,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/change-password", methods="GET|POST", name="user_change_password")
|
||||||
|
*/
|
||||||
|
public function changePassword(Request $request, UserPasswordEncoderInterface $encoder): Response
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
$form = $this->createForm(ChangePasswordType::class);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$user->setPassword($encoder->encodePassword($user, $form->get('newPassword')->getData()));
|
||||||
|
|
||||||
|
$this->getDoctrine()->getManager()->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('security_logout');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('user/change_password.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
241
app/src/DataFixtures/AppFixtures.php
Normal file
241
app/src/DataFixtures/AppFixtures.php
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\DataFixtures;
|
||||||
|
|
||||||
|
use App\Entity\Comment;
|
||||||
|
use App\Entity\Post;
|
||||||
|
use App\Entity\Tag;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
|
use Doctrine\Persistence\ObjectManager;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||||
|
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||||
|
use function Symfony\Component\String\u;
|
||||||
|
|
||||||
|
class AppFixtures extends Fixture
|
||||||
|
{
|
||||||
|
private $passwordEncoder;
|
||||||
|
private $slugger;
|
||||||
|
|
||||||
|
public function __construct(UserPasswordEncoderInterface $passwordEncoder, SluggerInterface $slugger)
|
||||||
|
{
|
||||||
|
$this->passwordEncoder = $passwordEncoder;
|
||||||
|
$this->slugger = $slugger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
$this->loadUsers($manager);
|
||||||
|
$this->loadTags($manager);
|
||||||
|
$this->loadPosts($manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadUsers(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
foreach ($this->getUserData() as [$fullname, $username, $password, $email, $roles]) {
|
||||||
|
$user = new User();
|
||||||
|
$user->setFullName($fullname);
|
||||||
|
$user->setUsername($username);
|
||||||
|
$user->setPassword($this->passwordEncoder->encodePassword($user, $password));
|
||||||
|
$user->setEmail($email);
|
||||||
|
$user->setRoles($roles);
|
||||||
|
|
||||||
|
$manager->persist($user);
|
||||||
|
$this->addReference($username, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadTags(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
foreach ($this->getTagData() as $index => $name) {
|
||||||
|
$tag = new Tag();
|
||||||
|
$tag->setName($name);
|
||||||
|
|
||||||
|
$manager->persist($tag);
|
||||||
|
$this->addReference('tag-'.$name, $tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadPosts(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
foreach ($this->getPostData() as [$title, $slug, $summary, $content, $publishedAt, $author, $tags]) {
|
||||||
|
$post = new Post();
|
||||||
|
$post->setTitle($title);
|
||||||
|
$post->setSlug($slug);
|
||||||
|
$post->setSummary($summary);
|
||||||
|
$post->setContent($content);
|
||||||
|
$post->setPublishedAt($publishedAt);
|
||||||
|
$post->setAuthor($author);
|
||||||
|
$post->addTag(...$tags);
|
||||||
|
|
||||||
|
foreach (range(1, 5) as $i) {
|
||||||
|
$comment = new Comment();
|
||||||
|
$comment->setAuthor($this->getReference('john_user'));
|
||||||
|
$comment->setContent($this->getRandomText(random_int(255, 512)));
|
||||||
|
$comment->setPublishedAt(new \DateTime('now + '.$i.'seconds'));
|
||||||
|
|
||||||
|
$post->addComment($comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->persist($post);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUserData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// $userData = [$fullname, $username, $password, $email, $roles];
|
||||||
|
['Jane Doe', 'jane_admin', 'kitten', 'jane_admin@symfony.com', ['ROLE_ADMIN']],
|
||||||
|
['Tom Doe', 'tom_admin', 'kitten', 'tom_admin@symfony.com', ['ROLE_ADMIN']],
|
||||||
|
['John Doe', 'john_user', 'kitten', 'john_user@symfony.com', ['ROLE_USER']],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTagData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'lorem',
|
||||||
|
'ipsum',
|
||||||
|
'consectetur',
|
||||||
|
'adipiscing',
|
||||||
|
'incididunt',
|
||||||
|
'labore',
|
||||||
|
'voluptate',
|
||||||
|
'dolore',
|
||||||
|
'pariatur',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPostData()
|
||||||
|
{
|
||||||
|
$posts = [];
|
||||||
|
foreach ($this->getPhrases() as $i => $title) {
|
||||||
|
// $postData = [$title, $slug, $summary, $content, $publishedAt, $author, $tags, $comments];
|
||||||
|
$posts[] = [
|
||||||
|
$title,
|
||||||
|
$this->slugger->slug($title)->lower(),
|
||||||
|
$this->getRandomText(),
|
||||||
|
$this->getPostContent(),
|
||||||
|
new \DateTime('now - '.$i.'days'),
|
||||||
|
// Ensure that the first post is written by Jane Doe to simplify tests
|
||||||
|
$this->getReference(['jane_admin', 'tom_admin'][0 === $i ? 0 : random_int(0, 1)]),
|
||||||
|
$this->getRandomTags(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPhrases(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Lorem ipsum dolor sit amet consectetur adipiscing elit',
|
||||||
|
'Pellentesque vitae velit ex',
|
||||||
|
'Mauris dapibus risus quis suscipit vulputate',
|
||||||
|
'Eros diam egestas libero eu vulputate risus',
|
||||||
|
'In hac habitasse platea dictumst',
|
||||||
|
'Morbi tempus commodo mattis',
|
||||||
|
'Ut suscipit posuere justo at vulputate',
|
||||||
|
'Ut eleifend mauris et risus ultrices egestas',
|
||||||
|
'Aliquam sodales odio id eleifend tristique',
|
||||||
|
'Urna nisl sollicitudin id varius orci quam id turpis',
|
||||||
|
'Nulla porta lobortis ligula vel egestas',
|
||||||
|
'Curabitur aliquam euismod dolor non ornare',
|
||||||
|
'Sed varius a risus eget aliquam',
|
||||||
|
'Nunc viverra elit ac laoreet suscipit',
|
||||||
|
'Pellentesque et sapien pulvinar consectetur',
|
||||||
|
'Ubi est barbatus nix',
|
||||||
|
'Abnobas sunt hilotaes de placidus vita',
|
||||||
|
'Ubi est audax amicitia',
|
||||||
|
'Eposs sunt solems de superbus fortis',
|
||||||
|
'Vae humani generis',
|
||||||
|
'Diatrias tolerare tanquam noster caesium',
|
||||||
|
'Teres talis saepe tractare de camerarius flavum sensorem',
|
||||||
|
'Silva de secundus galatae demitto quadra',
|
||||||
|
'Sunt accentores vitare salvus flavum parses',
|
||||||
|
'Potus sensim ad ferox abnoba',
|
||||||
|
'Sunt seculaes transferre talis camerarius fluctuies',
|
||||||
|
'Era brevis ratione est',
|
||||||
|
'Sunt torquises imitari velox mirabilis medicinaes',
|
||||||
|
'Mineralis persuadere omnes finises desiderium',
|
||||||
|
'Bassus fatalis classiss virtualiter transferre de flavum',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRandomText(int $maxLength = 255): string
|
||||||
|
{
|
||||||
|
$phrases = $this->getPhrases();
|
||||||
|
shuffle($phrases);
|
||||||
|
|
||||||
|
do {
|
||||||
|
$text = u('. ')->join($phrases)->append('.');
|
||||||
|
array_pop($phrases);
|
||||||
|
} while ($text->length() > $maxLength);
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPostContent(): string
|
||||||
|
{
|
||||||
|
return <<<'MARKDOWN'
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit, sed do eiusmod tempor
|
||||||
|
incididunt ut labore et **dolore magna aliqua**: Duis aute irure dolor in
|
||||||
|
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||||
|
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
|
||||||
|
deserunt mollit anim id est laborum.
|
||||||
|
|
||||||
|
* Ut enim ad minim veniam
|
||||||
|
* Quis nostrud exercitation *ullamco laboris*
|
||||||
|
* Nisi ut aliquip ex ea commodo consequat
|
||||||
|
|
||||||
|
Praesent id fermentum lorem. Ut est lorem, fringilla at accumsan nec, euismod at
|
||||||
|
nunc. Aenean mattis sollicitudin mattis. Nullam pulvinar vestibulum bibendum.
|
||||||
|
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
|
||||||
|
himenaeos. Fusce nulla purus, gravida ac interdum ut, blandit eget ex. Duis a
|
||||||
|
luctus dolor.
|
||||||
|
|
||||||
|
Integer auctor massa maximus nulla scelerisque accumsan. *Aliquam ac malesuada*
|
||||||
|
ex. Pellentesque tortor magna, vulputate eu vulputate ut, venenatis ac lectus.
|
||||||
|
Praesent ut lacinia sem. Mauris a lectus eget felis mollis feugiat. Quisque
|
||||||
|
efficitur, mi ut semper pulvinar, urna urna blandit massa, eget tincidunt augue
|
||||||
|
nulla vitae est.
|
||||||
|
|
||||||
|
Ut posuere aliquet tincidunt. Aliquam erat volutpat. **Class aptent taciti**
|
||||||
|
sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi
|
||||||
|
arcu orci, gravida eget aliquam eu, suscipit et ante. Morbi vulputate metus vel
|
||||||
|
ipsum finibus, ut dapibus massa feugiat. Vestibulum vel lobortis libero. Sed
|
||||||
|
tincidunt tellus et viverra scelerisque. Pellentesque tincidunt cursus felis.
|
||||||
|
Sed in egestas erat.
|
||||||
|
|
||||||
|
Aliquam pulvinar interdum massa, vel ullamcorper ante consectetur eu. Vestibulum
|
||||||
|
lacinia ac enim vel placerat. Integer pulvinar magna nec dui malesuada, nec
|
||||||
|
congue nisl dictum. Donec mollis nisl tortor, at congue erat consequat a. Nam
|
||||||
|
tempus elit porta, blandit elit vel, viverra lorem. Sed sit amet tellus
|
||||||
|
tincidunt, faucibus nisl in, aliquet libero.
|
||||||
|
MARKDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRandomTags(): array
|
||||||
|
{
|
||||||
|
$tagNames = $this->getTagData();
|
||||||
|
shuffle($tagNames);
|
||||||
|
$selectedTags = \array_slice($tagNames, 0, random_int(2, 4));
|
||||||
|
|
||||||
|
return array_map(function ($tagName) { return $this->getReference('tag-'.$tagName); }, $selectedTags);
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
47
app/src/Kernel.php
Normal file
47
app/src/Kernel.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?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;
|
||||||
|
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||||
|
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||||
|
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
|
||||||
|
|
||||||
|
class Kernel extends BaseKernel
|
||||||
|
{
|
||||||
|
use MicroKernelTrait;
|
||||||
|
|
||||||
|
protected function configureContainer(ContainerConfigurator $container): void
|
||||||
|
{
|
||||||
|
$container->import('../config/{packages}/*.yaml');
|
||||||
|
$container->import('../config/{packages}/'.$this->environment.'/*.yaml');
|
||||||
|
|
||||||
|
if (is_file(\dirname(__DIR__).'/config/services.yaml')) {
|
||||||
|
$container->import('../config/services.yaml');
|
||||||
|
$container->import('../config/{services}_'.$this->environment.'.yaml');
|
||||||
|
} elseif (is_file($path = \dirname(__DIR__).'/config/services.php')) {
|
||||||
|
(require $path)($container->withPath($path), $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configureRoutes(RoutingConfigurator $routes): void
|
||||||
|
{
|
||||||
|
$routes->import('../config/{routes}/'.$this->environment.'/*.yaml');
|
||||||
|
$routes->import('../config/{routes}/*.yaml');
|
||||||
|
|
||||||
|
if (is_file(\dirname(__DIR__).'/config/routes.yaml')) {
|
||||||
|
$routes->import('../config/routes.yaml');
|
||||||
|
} elseif (is_file($path = \dirname(__DIR__).'/config/routes.php')) {
|
||||||
|
(require $path)($routes->withPath($path), $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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;
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user