├── .gitignore ├── sources └── app │ ├── tests │ ├── .gitignore │ ├── bootstrap.php │ └── Controller │ │ ├── AbstractControllerWebTestCase.php │ │ └── PostControllerTest.php │ ├── src │ ├── Entity │ │ ├── .gitignore │ │ ├── Post.php │ │ └── User.php │ ├── Controller │ │ ├── .gitignore │ │ ├── IndexController.php │ │ ├── SecurityController.php │ │ └── PostController.php │ ├── Migrations │ │ ├── .gitignore │ │ ├── Version20190618154659.php │ │ ├── Version20190618080327.php │ │ └── Version20190618152017.php │ ├── Repository │ │ └── .gitignore │ ├── DataFixtures │ │ ├── AppFixtures.php │ │ └── UserFixtures.php │ ├── Exception │ │ └── HTTPExceptionListener.php │ ├── Security │ │ └── HashPasswordListener.php │ └── Kernel.php │ ├── translations │ └── .gitignore │ ├── config │ ├── packages │ │ ├── test │ │ │ ├── swiftmailer.yaml │ │ │ ├── routing.yaml │ │ │ ├── validator.yaml │ │ │ ├── framework.yaml │ │ │ ├── web_profiler.yaml │ │ │ └── monolog.yaml │ │ ├── dev │ │ │ ├── routing.yaml │ │ │ ├── web_profiler.yaml │ │ │ ├── swiftmailer.yaml │ │ │ ├── debug.yaml │ │ │ ├── easy_log_handler.yaml │ │ │ └── monolog.yaml │ │ ├── swiftmailer.yaml │ │ ├── routing.yaml │ │ ├── sensio_framework_extra.yaml │ │ ├── assets.yaml │ │ ├── ramsey_uuid_doctrine.yaml │ │ ├── twig.yaml │ │ ├── prod │ │ │ ├── webpack_encore.yaml │ │ │ ├── monolog.yaml │ │ │ └── doctrine.yaml │ │ ├── translation.yaml │ │ ├── doctrine_migrations.yaml │ │ ├── validator.yaml │ │ ├── fos_rest.yaml │ │ ├── framework.yaml │ │ ├── cache.yaml │ │ ├── doctrine.yaml │ │ ├── webpack_encore.yaml │ │ └── security.yaml │ ├── routes.yaml │ ├── routes │ │ ├── annotations.yaml │ │ └── dev │ │ │ ├── twig.yaml │ │ │ └── web_profiler.yaml │ ├── bootstrap.php │ ├── bundles.php │ └── services.yaml │ ├── .env.test │ ├── assets │ └── vue │ │ ├── api │ │ ├── security.js │ │ └── post.js │ │ ├── index.js │ │ ├── store │ │ ├── index.js │ │ ├── security.js │ │ └── post.js │ │ ├── views │ │ ├── Home.vue │ │ ├── Posts.vue │ │ └── Login.vue │ │ ├── components │ │ ├── Post.vue │ │ └── ErrorMessage.vue │ │ ├── router │ │ └── index.js │ │ └── App.vue │ ├── phpstan.neon │ ├── .eslintrc.json │ ├── bin │ ├── phpunit │ └── console │ ├── .gitignore │ ├── public │ ├── index.php │ └── .htaccess │ ├── package.json │ ├── templates │ └── base.html.twig │ ├── phpunit.xml.dist │ ├── phpcs.xml.dist │ ├── webpack.config.js │ ├── composer.json │ └── symfony.lock ├── .env.template ├── services └── mysql │ └── utf8mb4.cnf ├── README.md ├── LICENSE └── docker-compose.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /sources/app/tests/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/app/src/Entity/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/app/src/Controller/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/app/src/Migrations/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/app/src/Repository/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/app/translations/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/app/config/packages/test/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | disable_delivery: true 3 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | MYSQL_ROOT_PASSWORD=admin 2 | MYSQL_DATABASE=tutorial 3 | MYSQL_USER=foo 4 | MYSQL_PASSWORD=bar -------------------------------------------------------------------------------- /sources/app/config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /sources/app/config/packages/test/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /sources/app/config/routes.yaml: -------------------------------------------------------------------------------- 1 | #index: 2 | # path: / 3 | # controller: App\Controller\DefaultController::index 4 | -------------------------------------------------------------------------------- /sources/app/config/packages/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | url: '%env(MAILER_URL)%' 3 | spool: { type: 'memory' } 4 | -------------------------------------------------------------------------------- /sources/app/config/packages/test/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | not_compromised_password: false 4 | -------------------------------------------------------------------------------- /sources/app/config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: ../../src/Controller/ 3 | type: annotation 4 | -------------------------------------------------------------------------------- /sources/app/config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | utf8: true 5 | -------------------------------------------------------------------------------- /sources/app/config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /sources/app/config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /sources/app/config/routes/dev/twig.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@TwigBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /sources/app/config/packages/assets.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | assets: 3 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' 4 | -------------------------------------------------------------------------------- /sources/app/config/packages/ramsey_uuid_doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | types: 4 | uuid: 'Ramsey\Uuid\Doctrine\UuidType' 5 | -------------------------------------------------------------------------------- /sources/app/.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='s$cretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | -------------------------------------------------------------------------------- /sources/app/config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | -------------------------------------------------------------------------------- /sources/app/config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /sources/app/config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /sources/app/config/packages/prod/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | #webpack_encore: 2 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 3 | # Available in version 1.2 4 | #cache: true 5 | -------------------------------------------------------------------------------- /sources/app/config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: '%locale%' 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - '%locale%' 7 | -------------------------------------------------------------------------------- /sources/app/config/packages/dev/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | # See https://symfony.com/doc/current/email/dev_environment.html 2 | swiftmailer: 3 | # send all emails to a specific address 4 | #delivery_addresses: ['me@example.com'] 5 | -------------------------------------------------------------------------------- /sources/app/assets/vue/api/security.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export default { 4 | login(login, password) { 5 | return axios.post("/api/security/login", { 6 | username: login, 7 | password: password 8 | }); 9 | } 10 | } -------------------------------------------------------------------------------- /services/mysql/utf8mb4.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | default-character-set = utf8mb4 3 | 4 | [mysql] 5 | default-character-set = utf8mb4 6 | 7 | [mysqld] 8 | character-set-client-handshake = FALSE 9 | character-set-server = utf8mb4 10 | collation-server = utf8mb4_unicode_ci -------------------------------------------------------------------------------- /sources/app/assets/vue/api/post.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export default { 4 | create(message) { 5 | return axios.post("/api/posts", { 6 | message: message 7 | }); 8 | }, 9 | findAll() { 10 | return axios.get("/api/posts"); 11 | } 12 | }; -------------------------------------------------------------------------------- /sources/app/assets/vue/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | 6 | new Vue({ 7 | components: { App }, 8 | template: "", 9 | router, 10 | store 11 | }).$mount("#app"); 12 | -------------------------------------------------------------------------------- /sources/app/config/packages/dev/debug.yaml: -------------------------------------------------------------------------------- 1 | debug: 2 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 3 | # See the "server:dump" command to start a new server. 4 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 5 | -------------------------------------------------------------------------------- /sources/app/config/routes/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /sources/app/config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | dir_name: '%kernel.project_dir%/src/Migrations' 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | namespace: DoctrineMigrations 6 | -------------------------------------------------------------------------------- /sources/app/config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | 5 | # Enables validator auto-mapping support. 6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 7 | auto_mapping: 8 | App\Entity\: [] 9 | -------------------------------------------------------------------------------- /sources/app/assets/vue/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import SecurityModule from "./security"; 4 | import PostModule from "./post"; 5 | 6 | Vue.use(Vuex); 7 | 8 | export default new Vuex.Store({ 9 | modules: { 10 | security: SecurityModule, 11 | post: PostModule 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /sources/app/phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon 3 | - vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon 4 | parameters: 5 | excludes_analyse: 6 | - %currentWorkingDirectory%/src/Migrations/*.php 7 | - %currentWorkingDirectory%/src/Kernel.php -------------------------------------------------------------------------------- /sources/app/config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | # Output logs to Docker stderr by default. 6 | path: "php://stderr" 7 | #path: "%kernel.logs_dir%/%kernel.environment%.log" 8 | level: debug 9 | channels: ["!event"] 10 | -------------------------------------------------------------------------------- /sources/app/assets/vue/views/Home.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /sources/app/assets/vue/components/Post.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /sources/app/assets/vue/components/ErrorMessage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /sources/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "sourceType": "module", 6 | "parser": "babel-eslint" 7 | }, 8 | "env": { 9 | "browser": true, 10 | "es6": true 11 | }, 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:vue/recommended" 15 | ], 16 | "rules": { 17 | "indent": ["error", 2] 18 | } 19 | } -------------------------------------------------------------------------------- /sources/app/src/DataFixtures/AppFixtures.php: -------------------------------------------------------------------------------- 1 | flush(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sources/app/bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | symfony/framework-bundle ### 3 | /.env.local 4 | /.env.local.php 5 | /.env.*.local 6 | /public/bundles/ 7 | /var/ 8 | /vendor/ 9 | ###< symfony/framework-bundle ### 10 | 11 | ###> symfony/phpunit-bridge ### 12 | .phpunit 13 | /phpunit.xml 14 | ###< symfony/phpunit-bridge ### 15 | 16 | ###> symfony/web-server-bundle ### 17 | /.web-server-pid 18 | ###< symfony/web-server-bundle ### 19 | 20 | ###> squizlabs/php_codesniffer ### 21 | /.phpcs-cache 22 | /phpcs.xml 23 | ###< squizlabs/php_codesniffer ### 24 | 25 | ###> symfony/webpack-encore-bundle ### 26 | /node_modules/ 27 | /public/build/ 28 | npm-debug.log 29 | yarn-error.log 30 | ###< symfony/webpack-encore-bundle ### 31 | -------------------------------------------------------------------------------- /sources/app/config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Put the unique name of your app here: the prefix seed 4 | # is used to compute stable namespaces for cache keys. 5 | #prefix_seed: your_vendor_name/app_name 6 | 7 | # The app cache caches to the filesystem by default. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: ~ 20 | -------------------------------------------------------------------------------- /sources/app/config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | # Output logs to Docker stderr by default. 6 | path: "php://stderr" 7 | #path: "%kernel.logs_dir%/%kernel.environment%.log" 8 | level: debug 9 | channels: ["!event"] 10 | # uncomment to get logging in your browser 11 | # you may have to allow bigger header sizes in your Web server configuration 12 | #firephp: 13 | # type: firephp 14 | # level: info 15 | #chromephp: 16 | # type: chromephp 17 | # level: info 18 | console: 19 | type: console 20 | process_psr_3_messages: false 21 | channels: ["!event", "!doctrine", "!console"] 22 | -------------------------------------------------------------------------------- /sources/app/config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | # configure these for your database server 4 | driver: 'pdo_mysql' 5 | server_version: '5.7' 6 | charset: utf8mb4 7 | default_table_options: 8 | charset: utf8mb4 9 | collate: utf8mb4_unicode_ci 10 | 11 | url: '%env(resolve:DATABASE_URL)%' 12 | schema_filter: ~^(?!sessions)~ 13 | orm: 14 | auto_generate_proxy_classes: true 15 | naming_strategy: doctrine.orm.naming_strategy.underscore 16 | auto_mapping: true 17 | mappings: 18 | App: 19 | is_bundle: false 20 | type: annotation 21 | dir: '%kernel.project_dir%/src/Entity' 22 | prefix: 'App\Entity' 23 | alias: App 24 | -------------------------------------------------------------------------------- /sources/app/config/packages/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | webpack_encore: 2 | # The path where Encore is building the assets. 3 | # This should match Encore.setOutputPath() in webpack.config.js. 4 | output_path: '%kernel.project_dir%/public/build' 5 | # If multiple builds are defined (as shown below), you can disable the default build: 6 | # output_path: false 7 | 8 | # if using Encore.enableIntegrityHashes() specify the crossorigin attribute value (default: false, or use 'anonymous' or 'use-credentials') 9 | # crossorigin: 'anonymous' 10 | 11 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes). 12 | # To enable caching for the production environment, creating a webpack_encore.yaml in the config/packages/prod directory with this value set to true 13 | # Available in version 1.2 14 | #cache: 'false' 15 | -------------------------------------------------------------------------------- /sources/app/src/Exception/HTTPExceptionListener.php: -------------------------------------------------------------------------------- 1 | getException(); 17 | if (! ($exception instanceof HttpException) || strpos($event->getRequest()->getRequestUri(), '/api/') === false) { 18 | return; 19 | } 20 | 21 | $response = new JsonResponse(['error' => $exception->getMessage()]); 22 | $response->setStatusCode($exception->getStatusCode()); 23 | $event->setResponse($response); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sources/app/config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | nested: 9 | type: stream 10 | # Output logs to Docker stderr by default. 11 | path: "php://stderr" 12 | #path: "%kernel.logs_dir%/%kernel.environment%.log" 13 | level: debug 14 | console: 15 | type: console 16 | process_psr_3_messages: false 17 | channels: ["!event", "!doctrine"] 18 | deprecation: 19 | type: stream 20 | path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log" 21 | deprecation_filter: 22 | type: filter 23 | handler: deprecation 24 | max_level: info 25 | channels: ["php"] 26 | -------------------------------------------------------------------------------- /sources/app/public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 26 | $response->send(); 27 | $kernel->terminate($request, $response); 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **A new version with Symfony + GraphQL + Nuxt.js is available here:** https://github.com/thecodingmachine/symfony-boilerplate 2 | 3 | --- 4 | 5 | Source code of the tutorial [thecodingmachine.io/building-a-single-page-application-with-symfony-4-and-vuejs](https://thecodingmachine.io/building-a-single-page-application-with-symfony-4-and-vuejs). 6 | 7 | # Quick start 8 | 9 | If you want to try out the project just follow those steps: 10 | 11 | ```bash 12 | $ cp .env.template .env 13 | $ docker-compose up -d 14 | $ docker-compose exec app bash # executing bash inside app service 15 | $ composer install 16 | $ yarn install 17 | $ yarn dev 18 | $ php bin/console doctrine:migration:migrate 19 | $ php bin/console doctrine:fixtures:load 20 | ``` 21 | 22 | On MacOS, also update your `/etc/hosts` file with: 23 | 24 | ``` 25 | 127.0.0.1 app.localhost 26 | 127.0.0.1 phpmyadmin.app.localhost 27 | ``` 28 | 29 | You may now go to [http://app.localhost/](http://app.localhost/) and 30 | login using the following credentials: 31 | 32 | Login: `foo` 33 | Password: `bar` 34 | -------------------------------------------------------------------------------- /sources/app/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | run(); 17 | if (! $process->isSuccessful()) { 18 | throw new ProcessFailedException($process); 19 | } 20 | 21 | $process = new Process(['php', 'bin/console', 'doctrine:fixtures:load', '--no-interaction']); 22 | $process->run(); 23 | if (! $process->isSuccessful()) { 24 | throw new ProcessFailedException($process); 25 | } 26 | -------------------------------------------------------------------------------- /sources/app/config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | auto_generate_proxy_classes: false 4 | metadata_cache_driver: 5 | type: service 6 | id: doctrine.system_cache_provider 7 | query_cache_driver: 8 | type: service 9 | id: doctrine.system_cache_provider 10 | result_cache_driver: 11 | type: service 12 | id: doctrine.result_cache_provider 13 | 14 | services: 15 | doctrine.result_cache_provider: 16 | class: Symfony\Component\Cache\DoctrineProvider 17 | public: false 18 | arguments: 19 | - '@doctrine.result_cache_pool' 20 | doctrine.system_cache_provider: 21 | class: Symfony\Component\Cache\DoctrineProvider 22 | public: false 23 | arguments: 24 | - '@doctrine.system_cache_pool' 25 | 26 | framework: 27 | cache: 28 | pools: 29 | doctrine.result_cache_pool: 30 | adapter: cache.app 31 | doctrine.system_cache_pool: 32 | adapter: cache.system 33 | -------------------------------------------------------------------------------- /sources/app/assets/vue/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import store from "../store"; 4 | import Home from "../views/Home"; 5 | import Login from "../views/Login"; 6 | import Posts from "../views/Posts"; 7 | 8 | Vue.use(VueRouter); 9 | 10 | let router = new VueRouter({ 11 | mode: "history", 12 | routes: [ 13 | { path: "/home", component: Home }, 14 | { path: "/login", component: Login }, 15 | { path: "/posts", component: Posts, meta: { requiresAuth: true } }, 16 | { path: "*", redirect: "/home" } 17 | ], 18 | }); 19 | 20 | router.beforeEach((to, from, next) => { 21 | if (to.matched.some(record => record.meta.requiresAuth)) { 22 | // this route requires auth, check if logged in 23 | // if not, redirect to login page. 24 | if (store.getters["security/isAuthenticated"]) { 25 | next(); 26 | } else { 27 | next({ 28 | path: "/login", 29 | query: { redirect: to.fullPath } 30 | }); 31 | } 32 | } else { 33 | next(); // make sure to always call next()! 34 | } 35 | }); 36 | 37 | export default router; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 TheCodingMachine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sources/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@babel/plugin-transform-runtime": "^7.4.4", 4 | "@babel/runtime": "^7.4.5", 5 | "@symfony/webpack-encore": "^0.27.0", 6 | "babel-eslint": "^10.0.2", 7 | "core-js": "^3.0.0", 8 | "eslint": "^5.16.0", 9 | "eslint-loader": "^2.1.2", 10 | "eslint-plugin-vue": "^5.2.2", 11 | "vue-loader": "^15.7.0", 12 | "vue-template-compiler": "^2.6.10", 13 | "webpack-notifier": "^1.6.0" 14 | }, 15 | "license": "UNLICENSED", 16 | "private": true, 17 | "scripts": { 18 | "dev-server": "encore dev-server", 19 | "dev": "encore dev", 20 | "watch": "encore dev --watch", 21 | "build": "encore production --progress", 22 | "csfix": "eslint assets/vue --ext .js,.vue, --fix" 23 | }, 24 | "dependencies": { 25 | "axios": "^0.19.0", 26 | "vue": "^2.6.10", 27 | "vue-router": "^3.0.6", 28 | "vuex": "^3.1.1" 29 | }, 30 | "browserslist": [ 31 | "> 0.5%", 32 | "last 2 versions", 33 | "Firefox ESR", 34 | "not dead" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /sources/app/config/bootstrap.php: -------------------------------------------------------------------------------- 1 | =1.2) 9 | /*if (is_array($env = @include dirname(__DIR__).'/.env.local.php')) { 10 | foreach ($env as $k => $v) { 11 | $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v); 12 | } 13 | } elseif (!class_exists(Dotenv::class)) { 14 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 15 | } else { 16 | // load all the .env files 17 | (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); 18 | }*/ 19 | 20 | $_SERVER += $_ENV; 21 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 22 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 23 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 24 | -------------------------------------------------------------------------------- /sources/app/templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Symfony 4 with a Vue.js SPA 6 | 7 | {{ encore_entry_link_tags('app') }} 8 | 9 | 10 |
11 | 12 | 13 | 14 | {{ encore_entry_script_tags('app') }} 15 | 16 | -------------------------------------------------------------------------------- /sources/app/src/Migrations/Version20190618154659.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 23 | 24 | $this->addSql('CREATE TABLE sessions (sess_id VARCHAR(128) NOT NULL PRIMARY KEY, sess_data BLOB NOT NULL, sess_time INTEGER UNSIGNED NOT NULL, sess_lifetime MEDIUMINT NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB'); 25 | } 26 | 27 | public function down(Schema $schema) : void 28 | { 29 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 30 | 31 | $this->addSql('DROP TABLE sessions'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sources/app/src/DataFixtures/UserFixtures.php: -------------------------------------------------------------------------------- 1 | createUser($manager, self::DEFAULT_USER_LOGIN, self::DEFAULT_USER_PASSWORD, ['ROLE_FOO']); 24 | $this->createUser($manager, self::USER_LOGIN_ROLE_BAR, self::USER_PASSWORD_ROLE_BAR, ['ROLE_BAR']); 25 | } 26 | 27 | /** 28 | * @param string[] $roles 29 | */ 30 | private function createUser(ObjectManager $manager, string $login, string $password, array $roles): void 31 | { 32 | $userEntity = new User(); 33 | $userEntity->setLogin($login); 34 | $userEntity->setPlainPassword($password); 35 | $userEntity->setRoles($roles); 36 | $manager->persist($userEntity); 37 | $manager->flush(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sources/app/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | tests 23 | 24 | 25 | 26 | 27 | 28 | src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /sources/app/config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers 3 | encoders: 4 | App\Entity\User: 5 | algorithm: auto 6 | providers: 7 | in_memory: { memory: ~ } 8 | pdo: 9 | entity: 10 | class: App\Entity\User 11 | property: login 12 | firewalls: 13 | dev: 14 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 15 | security: false 16 | main: 17 | anonymous: true 18 | 19 | provider: pdo 20 | 21 | json_login: 22 | check_path: /api/security/login 23 | 24 | logout: 25 | path: /api/security/logout 26 | 27 | # activate different ways to authenticate 28 | 29 | # http_basic: true 30 | # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate 31 | 32 | # form_login: true 33 | # https://symfony.com/doc/current/security/form_login_setup.html 34 | 35 | # Easy way to control access for large sections of your site 36 | # Note: Only the *first* access control that matches will be used 37 | access_control: 38 | # - { path: ^/admin, roles: ROLE_ADMIN } 39 | # - { path: ^/profile, roles: ROLE_USER } -------------------------------------------------------------------------------- /sources/app/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 23 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 24 | } 25 | 26 | if ($input->hasParameterOption('--no-debug', true)) { 27 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 28 | } 29 | 30 | require dirname(__DIR__).'/config/bootstrap.php'; 31 | 32 | if ($_SERVER['APP_DEBUG']) { 33 | umask(0000); 34 | 35 | if (class_exists(Debug::class)) { 36 | Debug::enable(); 37 | } 38 | } 39 | 40 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 41 | $application = new Application($kernel); 42 | $application->run($input); 43 | -------------------------------------------------------------------------------- /sources/app/config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], 6 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 7 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 8 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 9 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 10 | Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], 11 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 12 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 13 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 14 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], 15 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 16 | Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true], 17 | Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], 18 | FOS\RestBundle\FOSRestBundle::class => ['all' => true], 19 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], 20 | ]; 21 | -------------------------------------------------------------------------------- /sources/app/src/Migrations/Version20190618080327.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 24 | 25 | $this->addSql('CREATE TABLE posts (id CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', message VARCHAR(255) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); 26 | } 27 | 28 | public function down(Schema $schema) : void 29 | { 30 | // this down() migration is auto-generated, please modify it to your needs 31 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 32 | 33 | $this->addSql('DROP TABLE posts'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sources/app/src/Migrations/Version20190618152017.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 24 | 25 | $this->addSql('CREATE TABLE users (id CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', login VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:simple_array)\', created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_1483A5E9AA08CB10 (login), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); 26 | } 27 | 28 | public function down(Schema $schema) : void 29 | { 30 | // this down() migration is auto-generated, please modify it to your needs 31 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 32 | 33 | $this->addSql('DROP TABLE users'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sources/app/src/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 24 | } 25 | 26 | /** 27 | * @throws JsonException 28 | * 29 | * @Route("/{vueRouting}", requirements={"vueRouting"="^(?!api|_(profiler|wdt)).*"}, name="index") 30 | */ 31 | public function indexAction(): Response 32 | { 33 | /** @var User|null $user */ 34 | $user = $this->getUser(); 35 | $data = null; 36 | if (! empty($user)) { 37 | $userClone = clone $user; 38 | $userClone->setPassword(''); 39 | $data = $this->serializer->serialize($userClone, JsonEncoder::FORMAT); 40 | } 41 | 42 | return $this->render('base.html.twig', [ 43 | 'isAuthenticated' => json_encode(! empty($user)), 44 | 'user' => $data ?? json_encode($data), 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sources/app/src/Controller/SecurityController.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 27 | } 28 | 29 | /** 30 | * @Route("/security/login", name="login") 31 | */ 32 | public function loginAction(): JsonResponse 33 | { 34 | /** @var User $user */ 35 | $user = $this->getUser(); 36 | $userClone = clone $user; 37 | $userClone->setPassword(''); 38 | $data = $this->serializer->serialize($userClone, JsonEncoder::FORMAT); 39 | 40 | return new JsonResponse($data, Response::HTTP_OK, [], true); 41 | } 42 | 43 | /** 44 | * @throws RuntimeException 45 | * 46 | * @Route("/security/logout", name="logout") 47 | */ 48 | public function logoutAction(): void 49 | { 50 | throw new RuntimeException('This should not be reached!'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sources/app/phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | src 13 | tests 14 | 15 | tests/dependencies/* 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 0 29 | 30 | 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | traefik: 6 | image: traefik:1.7 7 | command: --docker --docker.exposedbydefault=false 8 | ports: 9 | - "80:80" 10 | volumes: 11 | - /var/run/docker.sock:/var/run/docker.sock 12 | 13 | app: 14 | image: thecodingmachine/php:7.3-v2-apache-node10 15 | labels: 16 | - traefik.enable=true 17 | - traefik.backend=app 18 | - traefik.frontend.rule=Host:app.localhost 19 | environment: 20 | APACHE_DOCUMENT_ROOT: "public/" 21 | PHP_EXTENSION_XDEBUG: "1" 22 | PHP_INI_MEMORY_LIMIT: "1G" 23 | # Symfony 24 | APP_ENV: "dev" 25 | APP_SECRET: "8d2a5c935d8ef1c0e2b751147382bc75" 26 | DATABASE_URL: "mysql://$MYSQL_USER:$MYSQL_PASSWORD@mysql:3306/$MYSQL_DATABASE" 27 | volumes: 28 | - ./sources/app:/var/www/html:rw 29 | 30 | mysql: 31 | image: mysql:5.7 32 | environment: 33 | MYSQL_ROOT_PASSWORD: "$MYSQL_ROOT_PASSWORD" 34 | MYSQL_DATABASE: "$MYSQL_DATABASE" 35 | MYSQL_USER: "$MYSQL_USER" 36 | MYSQL_PASSWORD: "$MYSQL_PASSWORD" 37 | volumes: 38 | - mysql_data:/var/lib/mysql 39 | - ./services/mysql/utf8mb4.cnf:/etc/mysql/conf.d/utf8mb4.cnf:ro 40 | 41 | mysql_tests: 42 | image: mysql:5.7 43 | environment: 44 | MYSQL_ROOT_PASSWORD: "admin" 45 | MYSQL_DATABASE: "tests" 46 | MYSQL_USER: "foo" 47 | MYSQL_PASSWORD: "bar" 48 | volumes: 49 | - ./services/mysql/utf8mb4.cnf:/etc/mysql/conf.d/utf8mb4.cnf:ro 50 | 51 | phpmyadmin: 52 | image: phpmyadmin/phpmyadmin:4.7 53 | labels: 54 | - traefik.enable=true 55 | - traefik.backend=phpmyadmin 56 | - traefik.frontend.rule=Host:phpmyadmin.app.localhost 57 | environment: 58 | PMA_HOST: "mysql" 59 | PMA_USER: "$MYSQL_USER" 60 | PMA_PASSWORD: "$MYSQL_PASSWORD" 61 | 62 | volumes: 63 | 64 | mysql_data: 65 | driver: local -------------------------------------------------------------------------------- /sources/app/src/Entity/Post.php: -------------------------------------------------------------------------------- 1 | id = Uuid::uuid4(); 57 | $this->created = new DateTime('NOW'); 58 | } 59 | 60 | /** 61 | * @ORM\PreUpdate 62 | */ 63 | public function onPreUpdate(): void 64 | { 65 | $this->updated = new DateTime('NOW'); 66 | } 67 | 68 | public function getId(): UuidInterface 69 | { 70 | return $this->id; 71 | } 72 | 73 | public function getMessage(): string 74 | { 75 | return $this->message; 76 | } 77 | 78 | public function setMessage(string $message): void 79 | { 80 | $this->message = $message; 81 | } 82 | 83 | public function getCreated(): DateTime 84 | { 85 | return $this->created; 86 | } 87 | 88 | public function getUpdated(): ?DateTime 89 | { 90 | return $this->updated; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /sources/app/src/Security/HashPasswordListener.php: -------------------------------------------------------------------------------- 1 | passwordEncoder = $passwordEncoder; 21 | } 22 | 23 | public function prePersist(LifecycleEventArgs $args): void 24 | { 25 | $entity = $args->getEntity(); 26 | if (! $entity instanceof User) { 27 | return; 28 | } 29 | 30 | $this->encodePassword($entity); 31 | } 32 | 33 | public function preUpdate(LifecycleEventArgs $args): void 34 | { 35 | $entity = $args->getEntity(); 36 | if (! $entity instanceof User) { 37 | return; 38 | } 39 | 40 | $this->encodePassword($entity); 41 | // necessary to force the update to see the change 42 | $em = $args->getEntityManager(); 43 | $meta = $em->getClassMetadata(get_class($entity)); 44 | $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getSubscribedEvents() 51 | { 52 | return ['prePersist', 'preUpdate']; 53 | } 54 | 55 | private function encodePassword(User $entity): void 56 | { 57 | $plainPassword = $entity->getPlainPassword(); 58 | if ($plainPassword === null) { 59 | return; 60 | } 61 | 62 | $encoded = $this->passwordEncoder->encodePassword( 63 | $entity, 64 | $plainPassword 65 | ); 66 | 67 | $entity->setPassword($encoded); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sources/app/config/services.yaml: -------------------------------------------------------------------------------- 1 | # This file is the entry point to configure your own services. 2 | # Files in the packages/ subdirectory configure your dependencies. 3 | 4 | # Put parameters here that don't need to change on each machine where the app is deployed 5 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 6 | parameters: 7 | locale: 'en' 8 | 9 | services: 10 | # default configuration for services in *this* file 11 | _defaults: 12 | autowire: true # Automatically injects dependencies in your services. 13 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 14 | 15 | # makes classes in src/ available to be used as services 16 | # this creates a service per class whose id is the fully-qualified class name 17 | App\: 18 | resource: '../src/*' 19 | exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' 20 | 21 | # controllers are imported separately to make sure services can be injected 22 | # as action arguments even if you don't extend any base controller class 23 | App\Controller\: 24 | resource: '../src/Controller' 25 | tags: ['controller.service_arguments'] 26 | 27 | # add more service definitions when explicit configuration is needed 28 | # please note that last definitions always *replace* previous ones 29 | App\Exception\HTTPExceptionListener: 30 | tags: 31 | - { name: kernel.event_listener, event: kernel.exception } 32 | 33 | app.security.hash.password.listener: 34 | class: App\Security\HashPasswordListener 35 | tags: 36 | - { name: doctrine.event_subscriber } 37 | 38 | Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler: 39 | arguments: 40 | - !service { class: PDO, factory: 'database_connection:getWrappedConnection' } 41 | # If you get transaction issues (e.g. after login) uncomment the line below 42 | - { lock_mode: 1 } -------------------------------------------------------------------------------- /sources/app/tests/Controller/AbstractControllerWebTestCase.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 25 | } 26 | 27 | /** 28 | * @param mixed[] $data 29 | * 30 | * @throws JsonException 31 | */ 32 | protected function JSONRequest(string $method, string $uri, array $data = []): void 33 | { 34 | $this->client->request($method, $uri, [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($data)); 35 | } 36 | 37 | /** 38 | * @return mixed 39 | * 40 | * @throws JsonException 41 | */ 42 | protected function assertJSONResponse(Response $response, int $expectedStatusCode) 43 | { 44 | $this->assertEquals($expectedStatusCode, $response->getStatusCode()); 45 | $this->assertTrue($response->headers->contains('Content-Type', 'application/json')); 46 | $this->assertJson($response->getContent()); 47 | 48 | return json_decode($response->getContent(), true); 49 | } 50 | 51 | /** 52 | * @throws JsonException 53 | */ 54 | protected function login(string $username = UserFixtures::DEFAULT_USER_LOGIN, string $password = UserFixtures::DEFAULT_USER_PASSWORD): void 55 | { 56 | $this->client->request(Request::METHOD_POST, '/api/security/login', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode(['username' => $username, 'password' => $password])); 57 | $this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sources/app/src/Kernel.php: -------------------------------------------------------------------------------- 1 | getProjectDir() . '/config/bundles.php'; 24 | foreach ($contents as $class => $envs) { 25 | if ($envs[$this->environment] ?? $envs['all'] ?? false) { 26 | yield new $class(); 27 | } 28 | } 29 | } 30 | 31 | public function getProjectDir(): string 32 | { 33 | return dirname(__DIR__); 34 | } 35 | 36 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void 37 | { 38 | $container->addResource(new FileResource($this->getProjectDir() . '/config/bundles.php')); 39 | $container->setParameter('container.dumper.inline_class_loader', true); 40 | $confDir = $this->getProjectDir() . '/config'; 41 | 42 | $loader->load($confDir . '/{packages}/*' . self::CONFIG_EXTS, 'glob'); 43 | $loader->load($confDir . '/{packages}/' . $this->environment . '/**/*' . self::CONFIG_EXTS, 'glob'); 44 | $loader->load($confDir . '/{services}' . self::CONFIG_EXTS, 'glob'); 45 | $loader->load($confDir . '/{services}_' . $this->environment . self::CONFIG_EXTS, 'glob'); 46 | } 47 | 48 | protected function configureRoutes(RouteCollectionBuilder $routes): void 49 | { 50 | $confDir = $this->getProjectDir() . '/config'; 51 | 52 | $routes->import($confDir . '/{routes}/' . $this->environment . '/**/*' . self::CONFIG_EXTS, '/', 'glob'); 53 | $routes->import($confDir . '/{routes}/*' . self::CONFIG_EXTS, '/', 'glob'); 54 | $routes->import($confDir . '/{routes}' . self::CONFIG_EXTS, '/', 'glob'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sources/app/assets/vue/store/security.js: -------------------------------------------------------------------------------- 1 | import SecurityAPI from "../api/security"; 2 | 3 | const AUTHENTICATING = "AUTHENTICATING", 4 | AUTHENTICATING_SUCCESS = "AUTHENTICATING_SUCCESS", 5 | AUTHENTICATING_ERROR = "AUTHENTICATING_ERROR", 6 | PROVIDING_DATA_ON_REFRESH_SUCCESS = "PROVIDING_DATA_ON_REFRESH_SUCCESS"; 7 | 8 | export default { 9 | namespaced: true, 10 | state: { 11 | isLoading: false, 12 | error: null, 13 | isAuthenticated: false, 14 | user: null 15 | }, 16 | getters: { 17 | isLoading(state) { 18 | return state.isLoading; 19 | }, 20 | hasError(state) { 21 | return state.error !== null; 22 | }, 23 | error(state) { 24 | return state.error; 25 | }, 26 | isAuthenticated(state) { 27 | return state.isAuthenticated; 28 | }, 29 | hasRole(state) { 30 | return role => { 31 | return state.user.roles.indexOf(role) !== -1; 32 | } 33 | } 34 | }, 35 | mutations: { 36 | [AUTHENTICATING](state) { 37 | state.isLoading = true; 38 | state.error = null; 39 | state.isAuthenticated = false; 40 | state.user = null; 41 | }, 42 | [AUTHENTICATING_SUCCESS](state, user) { 43 | state.isLoading = false; 44 | state.error = null; 45 | state.isAuthenticated = true; 46 | state.user = user; 47 | }, 48 | [AUTHENTICATING_ERROR](state, error) { 49 | state.isLoading = false; 50 | state.error = error; 51 | state.isAuthenticated = false; 52 | state.user = null; 53 | }, 54 | [PROVIDING_DATA_ON_REFRESH_SUCCESS](state, payload) { 55 | state.isLoading = false; 56 | state.error = null; 57 | state.isAuthenticated = payload.isAuthenticated; 58 | state.user = payload.user; 59 | } 60 | }, 61 | actions: { 62 | async login({commit}, payload) { 63 | commit(AUTHENTICATING); 64 | try { 65 | let response = await SecurityAPI.login(payload.login, payload.password); 66 | commit(AUTHENTICATING_SUCCESS, response.data); 67 | return response.data; 68 | } catch (error) { 69 | commit(AUTHENTICATING_ERROR, error); 70 | return null; 71 | } 72 | }, 73 | onRefresh({commit}, payload) { 74 | commit(PROVIDING_DATA_ON_REFRESH_SUCCESS, payload); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /sources/app/src/Controller/PostController.php: -------------------------------------------------------------------------------- 1 | em = $em; 34 | $this->serializer = $serializer; 35 | } 36 | 37 | /** 38 | * @throws BadRequestHttpException 39 | * 40 | * @Rest\Post("/posts", name="createPost") 41 | * @IsGranted("ROLE_FOO") 42 | */ 43 | public function createAction(Request $request): JsonResponse 44 | { 45 | $message = $request->request->get('message'); 46 | if (empty($message)) { 47 | throw new BadRequestHttpException('message cannot be empty'); 48 | } 49 | $post = new Post(); 50 | $post->setMessage($message); 51 | $this->em->persist($post); 52 | $this->em->flush(); 53 | $data = $this->serializer->serialize($post, JsonEncoder::FORMAT); 54 | 55 | return new JsonResponse($data, Response::HTTP_CREATED, [], true); 56 | } 57 | 58 | /** 59 | * @Rest\Get("/posts", name="findAllPosts") 60 | */ 61 | public function findAllAction(): JsonResponse 62 | { 63 | $posts = $this->em->getRepository(Post::class)->findBy([], ['id' => 'DESC']); 64 | $data = $this->serializer->serialize($posts, JsonEncoder::FORMAT); 65 | 66 | return new JsonResponse($data, Response::HTTP_OK, [], true); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /sources/app/assets/vue/store/post.js: -------------------------------------------------------------------------------- 1 | import PostAPI from "../api/post"; 2 | 3 | const CREATING_POST = "CREATING_POST", 4 | CREATING_POST_SUCCESS = "CREATING_POST_SUCCESS", 5 | CREATING_POST_ERROR = "CREATING_POST_ERROR", 6 | FETCHING_POSTS = "FETCHING_POSTS", 7 | FETCHING_POSTS_SUCCESS = "FETCHING_POSTS_SUCCESS", 8 | FETCHING_POSTS_ERROR = "FETCHING_POSTS_ERROR"; 9 | 10 | export default { 11 | namespaced: true, 12 | state: { 13 | isLoading: false, 14 | error: null, 15 | posts: [] 16 | }, 17 | getters: { 18 | isLoading(state) { 19 | return state.isLoading; 20 | }, 21 | hasError(state) { 22 | return state.error !== null; 23 | }, 24 | error(state) { 25 | return state.error; 26 | }, 27 | hasPosts(state) { 28 | return state.posts.length > 0; 29 | }, 30 | posts(state) { 31 | return state.posts; 32 | } 33 | }, 34 | mutations: { 35 | [CREATING_POST](state) { 36 | state.isLoading = true; 37 | state.error = null; 38 | }, 39 | [CREATING_POST_SUCCESS](state, post) { 40 | state.isLoading = false; 41 | state.error = null; 42 | state.posts.unshift(post); 43 | }, 44 | [CREATING_POST_ERROR](state, error) { 45 | state.isLoading = false; 46 | state.error = error; 47 | state.posts = []; 48 | }, 49 | [FETCHING_POSTS](state) { 50 | state.isLoading = true; 51 | state.error = null; 52 | state.posts = []; 53 | }, 54 | [FETCHING_POSTS_SUCCESS](state, posts) { 55 | state.isLoading = false; 56 | state.error = null; 57 | state.posts = posts; 58 | }, 59 | [FETCHING_POSTS_ERROR](state, error) { 60 | state.isLoading = false; 61 | state.error = error; 62 | state.posts = []; 63 | } 64 | }, 65 | actions: { 66 | async create({ commit }, message) { 67 | commit(CREATING_POST); 68 | try { 69 | let response = await PostAPI.create(message); 70 | commit(CREATING_POST_SUCCESS, response.data); 71 | return response.data; 72 | } catch (error) { 73 | commit(CREATING_POST_ERROR, error); 74 | return null; 75 | } 76 | }, 77 | async findAll({ commit }) { 78 | commit(FETCHING_POSTS); 79 | try { 80 | let response = await PostAPI.findAll(); 81 | commit(FETCHING_POSTS_SUCCESS, response.data); 82 | return response.data; 83 | } catch (error) { 84 | commit(FETCHING_POSTS_ERROR, error); 85 | return null; 86 | } 87 | } 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /sources/app/tests/Controller/PostControllerTest.php: -------------------------------------------------------------------------------- 1 | JSONRequest(Request::METHOD_POST, '/api/posts'); 22 | $this->assertJSONResponse($this->client->getResponse(), Response::HTTP_UNAUTHORIZED); 23 | // test that sending a request while not having the role "ROLE_FOO" will result to a forbidden HTTP code. 24 | $this->login(UserFixtures::USER_LOGIN_ROLE_BAR, UserFixtures::USER_PASSWORD_ROLE_BAR); 25 | $this->JSONRequest(Request::METHOD_POST, '/api/posts'); 26 | $this->assertJSONResponse($this->client->getResponse(), Response::HTTP_FORBIDDEN); 27 | // test that sending a request while begin authenticated will result to a created HTTP code. 28 | $this->login(); 29 | $this->JSONRequest(Request::METHOD_POST, '/api/posts', ['message' => 'Hello world!']); 30 | $this->assertJSONResponse($this->client->getResponse(), Response::HTTP_CREATED); 31 | // test that sending no message will result to a bad request HTTP code. 32 | $this->JSONRequest(Request::METHOD_POST, '/api/posts'); 33 | $this->assertJSONResponse($this->client->getResponse(), Response::HTTP_BAD_REQUEST); 34 | } 35 | 36 | /** 37 | * @throws JsonException 38 | */ 39 | public function testFindAllPosts(): void 40 | { 41 | // test that sending a request without being authenticated will result to a unauthorized HTTP code. 42 | $this->client->request(Request::METHOD_GET, '/api/posts'); 43 | $this->assertJSONResponse($this->client->getResponse(), Response::HTTP_UNAUTHORIZED); 44 | // test that sending a request while begin authenticated will result to a OK HTTP code. 45 | $this->login(); 46 | $this->client->request(Request::METHOD_GET, '/api/posts'); 47 | $response = $this->client->getResponse(); 48 | $content = $this->assertJSONResponse($response, Response::HTTP_OK); 49 | $this->assertEquals(1, count($content)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /sources/app/assets/vue/App.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | -------------------------------------------------------------------------------- /sources/app/assets/vue/views/Posts.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 114 | -------------------------------------------------------------------------------- /sources/app/assets/vue/views/Login.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | -------------------------------------------------------------------------------- /sources/app/webpack.config.js: -------------------------------------------------------------------------------- 1 | let Encore = require('@symfony/webpack-encore'); 2 | 3 | // Manually configure the runtime environment if not already configured yet by the "encore" command. 4 | // It's useful when you use tools that rely on webpack.config.js file. 5 | if (!Encore.isRuntimeEnvironmentConfigured()) { 6 | Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); 7 | } 8 | 9 | Encore 10 | // directory where compiled assets will be stored 11 | .setOutputPath('public/build/') 12 | // public path used by the web server to access the output path 13 | .setPublicPath('/build') 14 | // only needed for CDN's or sub-directory deploy 15 | //.setManifestKeyPrefix('build/') 16 | 17 | /* 18 | * ENTRY CONFIG 19 | * 20 | * Add 1 entry for each "page" of your app 21 | * (including one that's included on every page - e.g. "app") 22 | * 23 | * Each entry will result in one JavaScript file (e.g. app.js) 24 | * and one CSS file (e.g. app.css) if you JavaScript imports CSS. 25 | */ 26 | .addEntry('app', './assets/vue/index.js') 27 | //.addEntry('page1', './assets/js/page1.js') 28 | //.addEntry('page2', './assets/js/page2.js') 29 | 30 | // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. 31 | .splitEntryChunks() 32 | 33 | // will require an extra script tag for runtime.js 34 | // but, you probably want this, unless you're building a single-page app 35 | //.enableSingleRuntimeChunk() 36 | .disableSingleRuntimeChunk() 37 | 38 | /* 39 | * FEATURE CONFIG 40 | * 41 | * Enable & configure other features below. For a full 42 | * list of features, see: 43 | * https://symfony.com/doc/current/frontend.html#adding-more-features 44 | */ 45 | .cleanupOutputBeforeBuild() 46 | .enableBuildNotifications() 47 | .enableSourceMaps(!Encore.isProduction()) 48 | // enables hashed filenames (e.g. app.abc123.css) 49 | .enableVersioning(Encore.isProduction()) 50 | 51 | // enables @babel/preset-env polyfills 52 | .configureBabel((babelConfig) => { 53 | babelConfig.plugins.push('@babel/plugin-transform-runtime'); 54 | }, { 55 | useBuiltIns: 'usage', 56 | corejs: 3 57 | }) 58 | 59 | // enables Vue.js support 60 | .enableVueLoader() 61 | 62 | // enables Sass/SCSS support 63 | //.enableSassLoader() 64 | 65 | // uncomment if you use TypeScript 66 | //.enableTypeScriptLoader() 67 | 68 | // uncomment to get integrity="..." attributes on your script & link tags 69 | // requires WebpackEncoreBundle 1.4 or higher 70 | .enableIntegrityHashes() 71 | 72 | // enable ESLint 73 | .addLoader({ 74 | enforce: 'pre', 75 | test: /\.(js|vue)$/, 76 | loader: 'eslint-loader', 77 | exclude: /node_modules/, 78 | options: { 79 | fix: true, 80 | emitError: true, 81 | emitWarning: true, 82 | }, 83 | }) 84 | 85 | // uncomment if you're having problems with a jQuery plugin 86 | //.autoProvidejQuery() 87 | 88 | // uncomment if you use API Platform Admin (composer req api-admin) 89 | //.enableReactPreset() 90 | //.addEntry('admin', './assets/js/admin.js') 91 | ; 92 | 93 | module.exports = Encore.getWebpackConfig(); -------------------------------------------------------------------------------- /sources/app/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "license": "proprietary", 4 | "require": { 5 | "php": "^7.1.3", 6 | "ext-ctype": "*", 7 | "ext-iconv": "*", 8 | "doctrine/doctrine-migrations-bundle": "^2.0", 9 | "friendsofsymfony/rest-bundle": "^2.5", 10 | "ramsey/uuid-doctrine": "^1.5", 11 | "sensio/framework-extra-bundle": "^5.1", 12 | "symfony/apache-pack": "^1.0", 13 | "symfony/asset": "4.3.*", 14 | "symfony/console": "4.3.*", 15 | "symfony/dotenv": "4.3.*", 16 | "symfony/expression-language": "4.3.*", 17 | "symfony/flex": "^1.1", 18 | "symfony/form": "4.3.*", 19 | "symfony/framework-bundle": "4.3.*", 20 | "symfony/http-client": "4.3.*", 21 | "symfony/intl": "4.3.*", 22 | "symfony/monolog-bundle": "^3.1", 23 | "symfony/orm-pack": "*", 24 | "symfony/process": "4.3.*", 25 | "symfony/security-bundle": "4.3.*", 26 | "symfony/serializer-pack": "*", 27 | "symfony/swiftmailer-bundle": "^3.1", 28 | "symfony/translation": "4.3.*", 29 | "symfony/twig-bundle": "4.3.*", 30 | "symfony/validator": "4.3.*", 31 | "symfony/web-link": "4.3.*", 32 | "symfony/webpack-encore-bundle": "^1.6", 33 | "symfony/yaml": "4.3.*", 34 | "thecodingmachine/safe": "^0.1.15" 35 | }, 36 | "require-dev": { 37 | "doctrine/coding-standard": "^6.0", 38 | "doctrine/doctrine-fixtures-bundle": "^3.2", 39 | "phpstan/phpstan": "^0.11.8", 40 | "squizlabs/php_codesniffer": "^3.4", 41 | "symfony/debug-pack": "*", 42 | "symfony/maker-bundle": "^1.0", 43 | "symfony/phpunit-bridge": "^4.3", 44 | "symfony/profiler-pack": "*", 45 | "symfony/test-pack": "*", 46 | "symfony/web-server-bundle": "4.3.*", 47 | "thecodingmachine/phpstan-safe-rule": "^0.1.3", 48 | "thecodingmachine/phpstan-strict-rules": "^0.11.2" 49 | }, 50 | "config": { 51 | "preferred-install": { 52 | "*": "dist" 53 | }, 54 | "sort-packages": true 55 | }, 56 | "autoload": { 57 | "psr-4": { 58 | "App\\": "src/" 59 | } 60 | }, 61 | "autoload-dev": { 62 | "psr-4": { 63 | "App\\Tests\\": "tests/" 64 | } 65 | }, 66 | "replace": { 67 | "paragonie/random_compat": "2.*", 68 | "symfony/polyfill-ctype": "*", 69 | "symfony/polyfill-iconv": "*", 70 | "symfony/polyfill-php71": "*", 71 | "symfony/polyfill-php70": "*", 72 | "symfony/polyfill-php56": "*" 73 | }, 74 | "scripts": { 75 | "csfix": "phpcbf --ignore=src/Migrations/**,src/Kernel.php", 76 | "cscheck": "phpcs --ignore=src/Migrations/**,src/Kernel.php", 77 | "phpstan": "phpstan analyse src/ -c phpstan.neon --level=7 --no-progress -vvv --memory-limit=1024M", 78 | "auto-scripts": { 79 | "cache:clear": "symfony-cmd", 80 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 81 | }, 82 | "post-install-cmd": [ 83 | "@auto-scripts" 84 | ], 85 | "post-update-cmd": [ 86 | "@auto-scripts" 87 | ] 88 | }, 89 | "conflict": { 90 | "symfony/symfony": "*" 91 | }, 92 | "extra": { 93 | "symfony": { 94 | "allow-contrib": false, 95 | "require": "4.3.*" 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /sources/app/public/.htaccess: -------------------------------------------------------------------------------- 1 | # Use the front controller as index file. It serves as a fallback solution when 2 | # every other rewrite/redirect fails (e.g. in an aliased environment without 3 | # mod_rewrite). Additionally, this reduces the matching process for the 4 | # start page (path "/") because otherwise Apache will apply the rewriting rules 5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). 6 | DirectoryIndex index.php 7 | 8 | # By default, Apache does not evaluate symbolic links if you did not enable this 9 | # feature in your server configuration. Uncomment the following line if you 10 | # install assets as symlinks or if you experience problems related to symlinks 11 | # when compiling LESS/Sass/CoffeScript assets. 12 | # Options FollowSymlinks 13 | 14 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve 15 | # to the front controller "/index.php" but be rewritten to "/index.php/index". 16 | 17 | Options -MultiViews 18 | 19 | 20 | 21 | RewriteEngine On 22 | 23 | # Determine the RewriteBase automatically and set it as environment variable. 24 | # If you are using Apache aliases to do mass virtual hosting or installed the 25 | # project in a subdirectory, the base path will be prepended to allow proper 26 | # resolution of the index.php file and to redirect to the correct URI. It will 27 | # work in environments without path prefix as well, providing a safe, one-size 28 | # fits all solution. But as you do not need it in this case, you can comment 29 | # the following 2 lines to eliminate the overhead. 30 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ 31 | RewriteRule ^(.*) - [E=BASE:%1] 32 | 33 | # Sets the HTTP_AUTHORIZATION header removed by Apache 34 | RewriteCond %{HTTP:Authorization} . 35 | RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 36 | 37 | # Redirect to URI without front controller to prevent duplicate content 38 | # (with and without `/index.php`). Only do this redirect on the initial 39 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an 40 | # endless redirect loop (request -> rewrite to front controller -> 41 | # redirect -> request -> ...). 42 | # So in case you get a "too many redirects" error or you always get redirected 43 | # to the start page because your Apache does not expose the REDIRECT_STATUS 44 | # environment variable, you have 2 choices: 45 | # - disable this feature by commenting the following 2 lines or 46 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the 47 | # following RewriteCond (best solution) 48 | RewriteCond %{ENV:REDIRECT_STATUS} ^$ 49 | RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] 50 | 51 | # If the requested filename exists, simply serve it. 52 | # We only want to let Apache serve files and not directories. 53 | RewriteCond %{REQUEST_FILENAME} -f 54 | RewriteRule ^ - [L] 55 | 56 | # Rewrite all other queries to the front controller. 57 | RewriteRule ^ %{ENV:BASE}/index.php [L] 58 | 59 | 60 | 61 | 62 | # When mod_rewrite is not available, we instruct a temporary redirect of 63 | # the start page to the front controller explicitly so that the website 64 | # and the generated links can still be used. 65 | RedirectMatch 307 ^/$ /index.php/ 66 | # RedirectTemp cannot be used instead 67 | 68 | 69 | -------------------------------------------------------------------------------- /sources/app/src/Entity/User.php: -------------------------------------------------------------------------------- 1 | roles = []; 76 | } 77 | 78 | /** 79 | * @ORM\PrePersist 80 | * 81 | * @throws Exception 82 | */ 83 | public function onPrePersist(): void 84 | { 85 | $this->id = Uuid::uuid4(); 86 | $this->created = new DateTime('NOW'); 87 | } 88 | 89 | /** 90 | * @ORM\PreUpdate 91 | */ 92 | public function onPreUpdate(): void 93 | { 94 | $this->updated = new DateTime('NOW'); 95 | } 96 | 97 | public function getId(): UuidInterface 98 | { 99 | return $this->id; 100 | } 101 | 102 | public function getLogin(): string 103 | { 104 | return $this->login; 105 | } 106 | 107 | public function setLogin(string $login): void 108 | { 109 | $this->login = $login; 110 | } 111 | 112 | public function getUsername(): string 113 | { 114 | return $this->login; 115 | } 116 | 117 | public function getPlainPassword(): ?string 118 | { 119 | return $this->plainPassword; 120 | } 121 | 122 | public function setPlainPassword(string $password): void 123 | { 124 | $this->plainPassword = $password; 125 | 126 | // forces the object to look "dirty" to Doctrine. Avoids 127 | // Doctrine *not* saving this entity, if only plainPassword changes 128 | $this->password = null; 129 | } 130 | 131 | public function getPassword(): ?string 132 | { 133 | return $this->password; 134 | } 135 | 136 | public function setPassword(string $password): void 137 | { 138 | $this->password = $password; 139 | } 140 | 141 | /** 142 | * @return null 143 | */ 144 | public function getSalt() 145 | { 146 | // The bcrypt algorithm doesn't require a separate salt. 147 | return null; 148 | } 149 | 150 | /** 151 | * @return string[] 152 | */ 153 | public function getRoles(): array 154 | { 155 | return $this->roles; 156 | } 157 | 158 | /** 159 | * @param string[] $roles 160 | */ 161 | public function setRoles(array $roles): void 162 | { 163 | $this->roles = $roles; 164 | } 165 | 166 | public function eraseCredentials(): void 167 | { 168 | $this->plainPassword = null; 169 | } 170 | 171 | public function getCreated(): DateTime 172 | { 173 | return $this->created; 174 | } 175 | 176 | public function getUpdated(): ?DateTime 177 | { 178 | return $this->updated; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /sources/app/symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "composer/xdebug-handler": { 3 | "version": "1.3.3" 4 | }, 5 | "dealerdirect/phpcodesniffer-composer-installer": { 6 | "version": "v0.5.0" 7 | }, 8 | "doctrine/annotations": { 9 | "version": "1.0", 10 | "recipe": { 11 | "repo": "github.com/symfony/recipes", 12 | "branch": "master", 13 | "version": "1.0", 14 | "ref": "cb4152ebcadbe620ea2261da1a1c5a9b8cea7672" 15 | }, 16 | "files": [ 17 | "config/routes/annotations.yaml" 18 | ] 19 | }, 20 | "doctrine/cache": { 21 | "version": "v1.8.0" 22 | }, 23 | "doctrine/coding-standard": { 24 | "version": "6.0.0" 25 | }, 26 | "doctrine/collections": { 27 | "version": "v1.6.2" 28 | }, 29 | "doctrine/common": { 30 | "version": "v2.10.0" 31 | }, 32 | "doctrine/data-fixtures": { 33 | "version": "v1.3.1" 34 | }, 35 | "doctrine/dbal": { 36 | "version": "v2.9.2" 37 | }, 38 | "doctrine/doctrine-bundle": { 39 | "version": "1.6", 40 | "recipe": { 41 | "repo": "github.com/symfony/recipes", 42 | "branch": "master", 43 | "version": "1.6", 44 | "ref": "02bc9e7994b70f4fda004131a0c78b7b1bf09789" 45 | }, 46 | "files": [ 47 | "config/packages/doctrine.yaml", 48 | "config/packages/prod/doctrine.yaml", 49 | "src/Entity/.gitignore", 50 | "src/Repository/.gitignore" 51 | ] 52 | }, 53 | "doctrine/doctrine-cache-bundle": { 54 | "version": "1.3.5" 55 | }, 56 | "doctrine/doctrine-fixtures-bundle": { 57 | "version": "3.0", 58 | "recipe": { 59 | "repo": "github.com/symfony/recipes", 60 | "branch": "master", 61 | "version": "3.0", 62 | "ref": "fc52d86631a6dfd9fdf3381d0b7e3df2069e51b3" 63 | }, 64 | "files": [ 65 | "src/DataFixtures/AppFixtures.php" 66 | ] 67 | }, 68 | "doctrine/doctrine-migrations-bundle": { 69 | "version": "1.2", 70 | "recipe": { 71 | "repo": "github.com/symfony/recipes", 72 | "branch": "master", 73 | "version": "1.2", 74 | "ref": "c1431086fec31f17fbcfe6d6d7e92059458facc1" 75 | }, 76 | "files": [ 77 | "config/packages/doctrine_migrations.yaml", 78 | "src/Migrations/.gitignore" 79 | ] 80 | }, 81 | "doctrine/event-manager": { 82 | "version": "v1.0.0" 83 | }, 84 | "doctrine/inflector": { 85 | "version": "v1.3.0" 86 | }, 87 | "doctrine/instantiator": { 88 | "version": "1.2.0" 89 | }, 90 | "doctrine/lexer": { 91 | "version": "1.0.2" 92 | }, 93 | "doctrine/migrations": { 94 | "version": "v2.1.0" 95 | }, 96 | "doctrine/orm": { 97 | "version": "v2.6.3" 98 | }, 99 | "doctrine/persistence": { 100 | "version": "1.1.1" 101 | }, 102 | "doctrine/reflection": { 103 | "version": "v1.0.0" 104 | }, 105 | "easycorp/easy-log-handler": { 106 | "version": "1.0", 107 | "recipe": { 108 | "repo": "github.com/symfony/recipes", 109 | "branch": "master", 110 | "version": "1.0", 111 | "ref": "70062abc2cd58794d2a90274502f81b55cd9951b" 112 | }, 113 | "files": [ 114 | "config/packages/dev/easy_log_handler.yaml" 115 | ] 116 | }, 117 | "egulias/email-validator": { 118 | "version": "2.1.8" 119 | }, 120 | "fig/link-util": { 121 | "version": "1.0.0" 122 | }, 123 | "friendsofsymfony/rest-bundle": { 124 | "version": "2.2", 125 | "recipe": { 126 | "repo": "github.com/symfony/recipes-contrib", 127 | "branch": "master", 128 | "version": "2.2", 129 | "ref": "258300d52be6ad59b32a888d5ddafbf9638540ff" 130 | }, 131 | "files": [ 132 | "config/packages/fos_rest.yaml" 133 | ] 134 | }, 135 | "jdorn/sql-formatter": { 136 | "version": "v1.2.17" 137 | }, 138 | "jean85/pretty-package-versions": { 139 | "version": "1.2" 140 | }, 141 | "monolog/monolog": { 142 | "version": "1.24.0" 143 | }, 144 | "nette/bootstrap": { 145 | "version": "v3.0.0" 146 | }, 147 | "nette/di": { 148 | "version": "v3.0.0" 149 | }, 150 | "nette/finder": { 151 | "version": "v2.5.0" 152 | }, 153 | "nette/neon": { 154 | "version": "v3.0.0" 155 | }, 156 | "nette/php-generator": { 157 | "version": "v3.2.2" 158 | }, 159 | "nette/robot-loader": { 160 | "version": "v3.2.0" 161 | }, 162 | "nette/schema": { 163 | "version": "v1.0.0" 164 | }, 165 | "nette/utils": { 166 | "version": "v3.0.1" 167 | }, 168 | "nikic/php-parser": { 169 | "version": "v4.2.2" 170 | }, 171 | "ocramius/proxy-manager": { 172 | "version": "2.1.1" 173 | }, 174 | "phpdocumentor/reflection-common": { 175 | "version": "1.0.1" 176 | }, 177 | "phpdocumentor/reflection-docblock": { 178 | "version": "4.3.1" 179 | }, 180 | "phpdocumentor/type-resolver": { 181 | "version": "0.4.0" 182 | }, 183 | "phpstan/phpdoc-parser": { 184 | "version": "0.3.5" 185 | }, 186 | "phpstan/phpstan": { 187 | "version": "0.11.8" 188 | }, 189 | "psr/cache": { 190 | "version": "1.0.1" 191 | }, 192 | "psr/container": { 193 | "version": "1.0.0" 194 | }, 195 | "psr/link": { 196 | "version": "1.0.0" 197 | }, 198 | "psr/log": { 199 | "version": "1.1.0" 200 | }, 201 | "ramsey/uuid": { 202 | "version": "3.8.0" 203 | }, 204 | "ramsey/uuid-doctrine": { 205 | "version": "1.3", 206 | "recipe": { 207 | "repo": "github.com/symfony/recipes-contrib", 208 | "branch": "master", 209 | "version": "1.3", 210 | "ref": "471aed0fbf5620b8d7f92b7a5ebbbf6c0945c27a" 211 | }, 212 | "files": [ 213 | "config/packages/ramsey_uuid_doctrine.yaml" 214 | ] 215 | }, 216 | "sensio/framework-extra-bundle": { 217 | "version": "5.2", 218 | "recipe": { 219 | "repo": "github.com/symfony/recipes", 220 | "branch": "master", 221 | "version": "5.2", 222 | "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" 223 | }, 224 | "files": [ 225 | "config/packages/sensio_framework_extra.yaml" 226 | ] 227 | }, 228 | "slevomat/coding-standard": { 229 | "version": "5.0.4" 230 | }, 231 | "squizlabs/php_codesniffer": { 232 | "version": "3.0", 233 | "recipe": { 234 | "repo": "github.com/symfony/recipes-contrib", 235 | "branch": "master", 236 | "version": "3.0", 237 | "ref": "0dc9cceda799fd3a08b96987e176a261028a3709" 238 | }, 239 | "files": [ 240 | "phpcs.xml.dist" 241 | ] 242 | }, 243 | "swiftmailer/swiftmailer": { 244 | "version": "v6.2.1" 245 | }, 246 | "symfony/apache-pack": { 247 | "version": "1.0", 248 | "recipe": { 249 | "repo": "github.com/symfony/recipes-contrib", 250 | "branch": "master", 251 | "version": "1.0", 252 | "ref": "c82bead70f9a4f656354a193df7bf0ca2114efa0" 253 | }, 254 | "files": [ 255 | "public/.htaccess" 256 | ] 257 | }, 258 | "symfony/asset": { 259 | "version": "v4.3.1" 260 | }, 261 | "symfony/browser-kit": { 262 | "version": "v4.3.1" 263 | }, 264 | "symfony/cache": { 265 | "version": "v4.3.1" 266 | }, 267 | "symfony/cache-contracts": { 268 | "version": "v1.1.1" 269 | }, 270 | "symfony/config": { 271 | "version": "v4.3.1" 272 | }, 273 | "symfony/console": { 274 | "version": "3.3", 275 | "recipe": { 276 | "repo": "github.com/symfony/recipes", 277 | "branch": "master", 278 | "version": "3.3", 279 | "ref": "482d233eb8de91ebd042992077bbd5838858890c" 280 | }, 281 | "files": [ 282 | "bin/console", 283 | "config/bootstrap.php" 284 | ] 285 | }, 286 | "symfony/css-selector": { 287 | "version": "v4.3.1" 288 | }, 289 | "symfony/debug": { 290 | "version": "v4.3.1" 291 | }, 292 | "symfony/debug-bundle": { 293 | "version": "4.1", 294 | "recipe": { 295 | "repo": "github.com/symfony/recipes", 296 | "branch": "master", 297 | "version": "4.1", 298 | "ref": "f8863cbad2f2e58c4b65fa1eac892ab189971bea" 299 | }, 300 | "files": [ 301 | "config/packages/dev/debug.yaml" 302 | ] 303 | }, 304 | "symfony/debug-pack": { 305 | "version": "v1.0.7" 306 | }, 307 | "symfony/dependency-injection": { 308 | "version": "v4.3.1" 309 | }, 310 | "symfony/doctrine-bridge": { 311 | "version": "v4.3.1" 312 | }, 313 | "symfony/dom-crawler": { 314 | "version": "v4.3.1" 315 | }, 316 | "symfony/dotenv": { 317 | "version": "v4.3.1" 318 | }, 319 | "symfony/event-dispatcher": { 320 | "version": "v4.3.1" 321 | }, 322 | "symfony/event-dispatcher-contracts": { 323 | "version": "v1.1.1" 324 | }, 325 | "symfony/expression-language": { 326 | "version": "v4.3.1" 327 | }, 328 | "symfony/filesystem": { 329 | "version": "v4.3.1" 330 | }, 331 | "symfony/finder": { 332 | "version": "v4.3.1" 333 | }, 334 | "symfony/flex": { 335 | "version": "1.0", 336 | "recipe": { 337 | "repo": "github.com/symfony/recipes", 338 | "branch": "master", 339 | "version": "1.0", 340 | "ref": "dc3fc2e0334a4137c47cfd5a3ececc601fa61a0b" 341 | }, 342 | "files": [ 343 | ".env" 344 | ] 345 | }, 346 | "symfony/form": { 347 | "version": "v4.3.1" 348 | }, 349 | "symfony/framework-bundle": { 350 | "version": "4.2", 351 | "recipe": { 352 | "repo": "github.com/symfony/recipes", 353 | "branch": "master", 354 | "version": "4.2", 355 | "ref": "f5cb896a182fa2b845c41d287d03cd61279b7ba5" 356 | }, 357 | "files": [ 358 | "config/bootstrap.php", 359 | "config/packages/cache.yaml", 360 | "config/packages/framework.yaml", 361 | "config/packages/test/framework.yaml", 362 | "config/services.yaml", 363 | "public/index.php", 364 | "src/Controller/.gitignore", 365 | "src/Kernel.php" 366 | ] 367 | }, 368 | "symfony/http-client": { 369 | "version": "v4.3.1" 370 | }, 371 | "symfony/http-client-contracts": { 372 | "version": "v1.1.3" 373 | }, 374 | "symfony/http-foundation": { 375 | "version": "v4.3.1" 376 | }, 377 | "symfony/http-kernel": { 378 | "version": "v4.3.1" 379 | }, 380 | "symfony/inflector": { 381 | "version": "v4.3.1" 382 | }, 383 | "symfony/intl": { 384 | "version": "v4.3.1" 385 | }, 386 | "symfony/maker-bundle": { 387 | "version": "1.0", 388 | "recipe": { 389 | "repo": "github.com/symfony/recipes", 390 | "branch": "master", 391 | "version": "1.0", 392 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 393 | } 394 | }, 395 | "symfony/mime": { 396 | "version": "v4.3.1" 397 | }, 398 | "symfony/monolog-bridge": { 399 | "version": "v4.3.1" 400 | }, 401 | "symfony/monolog-bundle": { 402 | "version": "3.3", 403 | "recipe": { 404 | "repo": "github.com/symfony/recipes", 405 | "branch": "master", 406 | "version": "3.3", 407 | "ref": "6240c6d43e8237a32452f057f81816820fd56ab6" 408 | }, 409 | "files": [ 410 | "config/packages/dev/monolog.yaml", 411 | "config/packages/prod/monolog.yaml", 412 | "config/packages/test/monolog.yaml" 413 | ] 414 | }, 415 | "symfony/options-resolver": { 416 | "version": "v4.3.1" 417 | }, 418 | "symfony/orm-pack": { 419 | "version": "v1.0.6" 420 | }, 421 | "symfony/phpunit-bridge": { 422 | "version": "4.3", 423 | "recipe": { 424 | "repo": "github.com/symfony/recipes", 425 | "branch": "master", 426 | "version": "4.3", 427 | "ref": "b0582341f1df39aaf3a9a866cdbe49937da35984" 428 | }, 429 | "files": [ 430 | ".env.test", 431 | "bin/phpunit", 432 | "config/bootstrap.php", 433 | "phpunit.xml.dist", 434 | "tests/.gitignore" 435 | ] 436 | }, 437 | "symfony/polyfill-intl-icu": { 438 | "version": "v1.11.0" 439 | }, 440 | "symfony/polyfill-intl-idn": { 441 | "version": "v1.11.0" 442 | }, 443 | "symfony/polyfill-mbstring": { 444 | "version": "v1.11.0" 445 | }, 446 | "symfony/polyfill-php72": { 447 | "version": "v1.11.0" 448 | }, 449 | "symfony/polyfill-php73": { 450 | "version": "v1.11.0" 451 | }, 452 | "symfony/process": { 453 | "version": "v4.3.1" 454 | }, 455 | "symfony/profiler-pack": { 456 | "version": "v1.0.4" 457 | }, 458 | "symfony/property-access": { 459 | "version": "v4.3.1" 460 | }, 461 | "symfony/property-info": { 462 | "version": "v4.3.1" 463 | }, 464 | "symfony/routing": { 465 | "version": "4.2", 466 | "recipe": { 467 | "repo": "github.com/symfony/recipes", 468 | "branch": "master", 469 | "version": "4.2", 470 | "ref": "5374e24d508ba8fd6ba9eb15170255fdb778316a" 471 | }, 472 | "files": [ 473 | "config/packages/dev/routing.yaml", 474 | "config/packages/routing.yaml", 475 | "config/packages/test/routing.yaml", 476 | "config/routes.yaml" 477 | ] 478 | }, 479 | "symfony/security-bundle": { 480 | "version": "3.3", 481 | "recipe": { 482 | "repo": "github.com/symfony/recipes", 483 | "branch": "master", 484 | "version": "3.3", 485 | "ref": "9a2034eca6d83d9cda632014e06995b8d9d9fd09" 486 | }, 487 | "files": [ 488 | "config/packages/security.yaml" 489 | ] 490 | }, 491 | "symfony/security-core": { 492 | "version": "v4.3.1" 493 | }, 494 | "symfony/security-csrf": { 495 | "version": "v4.3.1" 496 | }, 497 | "symfony/security-guard": { 498 | "version": "v4.3.1" 499 | }, 500 | "symfony/security-http": { 501 | "version": "v4.3.1" 502 | }, 503 | "symfony/serializer": { 504 | "version": "v4.3.1" 505 | }, 506 | "symfony/serializer-pack": { 507 | "version": "v1.0.2" 508 | }, 509 | "symfony/service-contracts": { 510 | "version": "v1.1.2" 511 | }, 512 | "symfony/stopwatch": { 513 | "version": "v4.3.1" 514 | }, 515 | "symfony/swiftmailer-bundle": { 516 | "version": "2.5", 517 | "recipe": { 518 | "repo": "github.com/symfony/recipes", 519 | "branch": "master", 520 | "version": "2.5", 521 | "ref": "3db029c03e452b4a23f7fc45cec7c922c2247eb8" 522 | }, 523 | "files": [ 524 | "config/packages/dev/swiftmailer.yaml", 525 | "config/packages/swiftmailer.yaml", 526 | "config/packages/test/swiftmailer.yaml" 527 | ] 528 | }, 529 | "symfony/templating": { 530 | "version": "v4.3.1" 531 | }, 532 | "symfony/test-pack": { 533 | "version": "v1.0.5" 534 | }, 535 | "symfony/translation": { 536 | "version": "3.3", 537 | "recipe": { 538 | "repo": "github.com/symfony/recipes", 539 | "branch": "master", 540 | "version": "3.3", 541 | "ref": "1fb02a6e1c8f3d4232cce485c9afa868d63b115a" 542 | }, 543 | "files": [ 544 | "config/packages/translation.yaml", 545 | "translations/.gitignore" 546 | ] 547 | }, 548 | "symfony/translation-contracts": { 549 | "version": "v1.1.2" 550 | }, 551 | "symfony/twig-bridge": { 552 | "version": "v4.3.1" 553 | }, 554 | "symfony/twig-bundle": { 555 | "version": "3.3", 556 | "recipe": { 557 | "repo": "github.com/symfony/recipes", 558 | "branch": "master", 559 | "version": "3.3", 560 | "ref": "369b5b29dc52b2c190002825ae7ec24ab6f962dd" 561 | }, 562 | "files": [ 563 | "config/packages/twig.yaml", 564 | "config/routes/dev/twig.yaml", 565 | "templates/base.html.twig" 566 | ] 567 | }, 568 | "symfony/validator": { 569 | "version": "4.3", 570 | "recipe": { 571 | "repo": "github.com/symfony/recipes", 572 | "branch": "master", 573 | "version": "4.3", 574 | "ref": "48a8e5ef925e82aa7d665c887cd1fa60f8b7921d" 575 | }, 576 | "files": [ 577 | "config/packages/test/validator.yaml", 578 | "config/packages/validator.yaml" 579 | ] 580 | }, 581 | "symfony/var-dumper": { 582 | "version": "v4.3.1" 583 | }, 584 | "symfony/var-exporter": { 585 | "version": "v4.3.1" 586 | }, 587 | "symfony/web-link": { 588 | "version": "v4.3.1" 589 | }, 590 | "symfony/web-profiler-bundle": { 591 | "version": "3.3", 592 | "recipe": { 593 | "repo": "github.com/symfony/recipes", 594 | "branch": "master", 595 | "version": "3.3", 596 | "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6" 597 | }, 598 | "files": [ 599 | "config/packages/dev/web_profiler.yaml", 600 | "config/packages/test/web_profiler.yaml", 601 | "config/routes/dev/web_profiler.yaml" 602 | ] 603 | }, 604 | "symfony/web-server-bundle": { 605 | "version": "3.3", 606 | "recipe": { 607 | "repo": "github.com/symfony/recipes", 608 | "branch": "master", 609 | "version": "3.3", 610 | "ref": "dae9b39fd6717970be7601101ce5aa960bf53d9a" 611 | } 612 | }, 613 | "symfony/webpack-encore-bundle": { 614 | "version": "1.0", 615 | "recipe": { 616 | "repo": "github.com/symfony/recipes", 617 | "branch": "master", 618 | "version": "1.0", 619 | "ref": "e33393e32759195f8e10c9d4faceb0d232a7e1de" 620 | }, 621 | "files": [ 622 | "assets/css/app.css", 623 | "assets/js/app.js", 624 | "config/packages/assets.yaml", 625 | "config/packages/prod/webpack_encore.yaml", 626 | "config/packages/webpack_encore.yaml", 627 | "package.json", 628 | "webpack.config.js" 629 | ] 630 | }, 631 | "symfony/yaml": { 632 | "version": "v4.3.1" 633 | }, 634 | "thecodingmachine/phpstan-safe-rule": { 635 | "version": "v0.1.3" 636 | }, 637 | "thecodingmachine/phpstan-strict-rules": { 638 | "version": "v0.11.2" 639 | }, 640 | "thecodingmachine/safe": { 641 | "version": "v0.1.15" 642 | }, 643 | "twig/twig": { 644 | "version": "v2.11.2" 645 | }, 646 | "webmozart/assert": { 647 | "version": "1.4.0" 648 | }, 649 | "willdurand/jsonp-callback-validator": { 650 | "version": "v1.1.0" 651 | }, 652 | "willdurand/negotiation": { 653 | "version": "v2.3.1" 654 | }, 655 | "zendframework/zend-code": { 656 | "version": "3.3.1" 657 | }, 658 | "zendframework/zend-eventmanager": { 659 | "version": "3.2.1" 660 | } 661 | } 662 | --------------------------------------------------------------------------------