├── .env ├── .env.dev ├── .env.prod ├── .env.stage ├── .env.test ├── .gitignore ├── .gitlab-ci.yml ├── README.md ├── assets └── bootstrap.min.css ├── bin ├── console └── phpunit ├── ci ├── check-connection ├── fetch-cluster-credentials └── wait-for-rollout ├── composer.json ├── composer.lock ├── config ├── bootstrap.php ├── bundles.php ├── packages │ ├── cache.yaml │ ├── dev │ │ ├── debug.yaml │ │ ├── easy_log_handler.yaml │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ ├── swiftmailer.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ └── monolog.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── sensio_framework_extra.yaml │ ├── swiftmailer.yaml │ ├── test │ │ ├── framework.yaml │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ ├── swiftmailer.yaml │ │ └── web_profiler.yaml │ ├── translation.yaml │ ├── twig.yaml │ └── validator.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ └── dev │ │ ├── twig.yaml │ │ └── web_profiler.yaml └── services.yaml ├── docker-compose.yml ├── docker ├── helm-gke │ └── Dockerfile ├── mysql │ ├── Dockerfile │ ├── data │ │ └── .gitkeep │ └── init │ │ └── products.sql ├── nginx │ ├── Dockerfile │ └── default.conf └── php-fpm │ ├── Dockerfile │ └── default.ini ├── helm ├── .helmignore ├── Chart.yaml ├── templates │ ├── deployment.tpl │ ├── ingress.tpl │ ├── mysql.deployment.yaml │ ├── mysql.service.yaml │ ├── nginx.deployment.yaml │ ├── nginx.ingress.yaml │ ├── nginx.service.yaml │ ├── php-fpm.deployment.yaml │ ├── php-fpm.service.yaml │ └── service.tpl └── values.yaml ├── phpunit.xml.dist ├── public └── index.php ├── src ├── Controller │ ├── .gitignore │ ├── HealthController.php │ └── ProductController.php ├── Entity │ ├── .gitignore │ └── Product.php ├── Form │ └── ProductAddType.php ├── Kernel.php ├── Migrations │ └── .gitignore └── Repository │ ├── .gitignore │ └── ProductRepository.php ├── symfony.lock ├── templates ├── base.html.twig └── product │ ├── add.html.twig │ └── show.html.twig ├── tests ├── .gitignore ├── EndToEnd │ ├── collection.json │ ├── env.dev.json │ └── env.stage.json ├── Integration │ └── Controller │ │ └── ProductControllerTest.php ├── Support │ ├── CsrfTokenManager.php │ ├── Database.php │ ├── HttpClient.php │ ├── Kernel.php │ └── Router.php └── Unit │ └── Controller │ ├── HealthControllerTest.php │ └── ProductControllerTest.php └── translations └── .gitignore /.env: -------------------------------------------------------------------------------- 1 | ###> docker ### 2 | NGINX_VERSION=1.15 3 | PHP_VERSION=7.3 4 | MYSQL_VERSION=5.7 5 | MYSQL_DATABASE=app 6 | MYSQL_ROOT_PASSWORD=fP8cz7Q63nV 7 | ###< docker ### 8 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | APP_ENV=dev 3 | APP_SECRET=4c0ac06a5564e2ccb8b951f31db34099 4 | #TRUSTED_PROXIES=127.0.0.1,127.0.0.2 5 | #TRUSTED_HOSTS='^localhost|example\.com$' 6 | ###< symfony/framework-bundle ### 7 | 8 | ###> doctrine/doctrine-bundle ### 9 | # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 10 | # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" 11 | # Configure your db driver and server_version in config/packages/doctrine.yaml 12 | DATABASE_URL=mysql://root:fP8cz7Q63nV@mysql:3306/app 13 | ###< doctrine/doctrine-bundle ### 14 | 15 | ###> symfony/swiftmailer-bundle ### 16 | # For Gmail as a transport, use: "gmail://username:password@localhost" 17 | # For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode=" 18 | # Delivery is disabled by default via "null://localhost" 19 | MAILER_URL=null://localhost 20 | ###< symfony/swiftmailer-bundle ### 21 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | APP_ENV=prod 3 | APP_SECRET=6c8315b702166496a62a29ab8a643a2d292403df 4 | ###< symfony/framework-bundle ### 5 | -------------------------------------------------------------------------------- /.env.stage: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | APP_ENV=stage 3 | APP_SECRET=7792eb5e9a9e5e1d6b9a5b98b3e1ae130552b13e 4 | ###< symfony/framework-bundle ### 5 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | APP_ENV=test 3 | APP_SECRET=74d279aba0165f4bf0fd129f1e9300cf0ba7d607 4 | KERNEL_CLASS=App\Kernel 5 | ###< symfony/framework-bundle ### 6 | 7 | ###> doctrine/doctrine-bundle ### 8 | DATABASE_URL=mysql://root:fP8cz7Q63nV@mysql:3306/app 9 | ###< doctrine/doctrine-bundle ### 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 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 | ###> PhpStorm ### 21 | /.idea 22 | ###< PhpStorm ### 23 | 24 | ###> docker ### 25 | /docker/mysql/data/* 26 | !/docker/mysql/data/.gitkeep 27 | ###< docker ### 28 | 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: docker:latest 2 | 3 | services: 4 | - docker:dind 5 | 6 | stages: 7 | - build 8 | - test 9 | - registry-update 10 | - stage-deployment 11 | - end-to-end-tests 12 | - production-deployment 13 | 14 | .build-template: &buildTemplate 15 | stage: build 16 | script: 17 | # Build the image 18 | - docker build --build-arg VERSION=$BUILD_VERSION --target $BUILD_TARGET -t $BUILD_IMAGE:$BUILD_TARGET -f $BUILD_DOCKERFILE $BUILD_CONTEXT 19 | after_script: 20 | # Save the image as an artifact 21 | - mkdir -p build/$BUILD_IMAGE 22 | - docker save $BUILD_IMAGE:$BUILD_TARGET -o build/$BUILD_IMAGE/$BUILD_TARGET.tar 23 | artifacts: 24 | name: $CI_JOB_NAME-${CI_COMMIT_SHA:0:8} 25 | expire_in: 1 day 26 | paths: 27 | - build/$BUILD_IMAGE 28 | dependencies: [] 29 | 30 | build-php-fpm-test: 31 | <<: *buildTemplate 32 | variables: 33 | BUILD_IMAGE: symfony-dummy-project-php-fpm 34 | BUILD_TARGET: test 35 | BUILD_DOCKERFILE: docker/php-fpm/Dockerfile 36 | BUILD_CONTEXT: . 37 | before_script: 38 | # Export build version 39 | - source .env && export BUILD_VERSION=$PHP_VERSION 40 | 41 | build-php-fpm-prod: 42 | <<: *buildTemplate 43 | variables: 44 | BUILD_IMAGE: symfony-dummy-project-php-fpm 45 | BUILD_TARGET: prod 46 | BUILD_DOCKERFILE: docker/php-fpm/Dockerfile 47 | BUILD_CONTEXT: . 48 | before_script: 49 | # Export build version 50 | - source .env && export BUILD_VERSION=$PHP_VERSION 51 | 52 | build-nginx-prod: 53 | <<: *buildTemplate 54 | variables: 55 | BUILD_IMAGE: symfony-dummy-project-nginx 56 | BUILD_TARGET: prod 57 | BUILD_DOCKERFILE: docker/nginx/Dockerfile 58 | BUILD_CONTEXT: . 59 | before_script: 60 | # Export build version 61 | - source .env && export BUILD_VERSION=$NGINX_VERSION 62 | 63 | build-mysql-prod: 64 | <<: *buildTemplate 65 | variables: 66 | BUILD_IMAGE: symfony-dummy-project-mysql 67 | BUILD_TARGET: prod 68 | BUILD_DOCKERFILE: docker/mysql/Dockerfile 69 | BUILD_CONTEXT: docker/mysql 70 | before_script: 71 | # Export build version 72 | - source .env && export BUILD_VERSION=$MYSQL_VERSION 73 | 74 | unit-tests: 75 | stage: test 76 | before_script: 77 | # Load php image from the build stage 78 | - docker load -i build/symfony-dummy-project-php-fpm/test.tar 79 | script: 80 | - docker run --rm symfony-dummy-project-php-fpm:test bin/phpunit --testsuit unit --testdox 81 | dependencies: 82 | - build-php-fpm-test 83 | 84 | integration-tests: 85 | stage: test 86 | before_script: 87 | # Load php image from the build stage 88 | - docker load -i build/symfony-dummy-project-php-fpm/test.tar 89 | - docker load -i build/symfony-dummy-project-mysql/prod.tar 90 | # Start mysql 91 | - source .env 92 | - docker network create symfony-dummy-project-net 93 | - docker run --rm -d --network symfony-dummy-project-net --network-alias mysql -e MYSQL_DATABASE=$MYSQL_DATABASE -e MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD symfony-dummy-project-mysql:prod 94 | script: 95 | - docker run --rm --network symfony-dummy-project-net symfony-dummy-project-php-fpm:test sh -c "sh ci/check-connection mysql 3306 && bin/phpunit --testsuit integration --testdox" 96 | dependencies: 97 | - build-php-fpm-test 98 | - build-mysql-prod 99 | 100 | .registry-update-template: ®istryUpdateTemplate 101 | stage: registry-update 102 | before_script: 103 | # Load image from the build stage 104 | - docker load -i build/$BUILD_IMAGE/$BUILD_TARGET.tar 105 | script: 106 | # Tag the image 107 | - docker tag $BUILD_IMAGE:$BUILD_TARGET babenkoivan/$BUILD_IMAGE:${CI_COMMIT_SHA:0:8} 108 | # Push the image 109 | - echo $CONTAINER_REGISTRY_PASSWORD | docker login -u $CONTAINER_REGISTRY_USER --password-stdin 110 | - docker push babenkoivan/$BUILD_IMAGE:${CI_COMMIT_SHA:0:8} 111 | only: 112 | - master 113 | 114 | registry-update-php-fpm: 115 | <<: *registryUpdateTemplate 116 | variables: 117 | BUILD_IMAGE: symfony-dummy-project-php-fpm 118 | BUILD_TARGET: prod 119 | dependencies: 120 | - build-php-fpm-prod 121 | 122 | registry-update-nginx: 123 | <<: *registryUpdateTemplate 124 | variables: 125 | BUILD_IMAGE: symfony-dummy-project-nginx 126 | BUILD_TARGET: prod 127 | dependencies: 128 | - build-nginx-prod 129 | 130 | registry-update-mysql: 131 | <<: *registryUpdateTemplate 132 | variables: 133 | BUILD_IMAGE: symfony-dummy-project-mysql 134 | BUILD_TARGET: prod 135 | dependencies: 136 | - build-mysql-prod 137 | 138 | .deploy-template: &deployTemplate 139 | image: babenkoivan/helm-gke 140 | before_script: 141 | # Fetch cluster credentials 142 | - sh ci/fetch-cluster-credentials $SERVICE_ACCOUNT_KEY $CLUSTER_NAME $CLUSTER_ZONE 143 | script: 144 | - helm upgrade symfony-dummy-project helm --install --set-string phpfpm.env.plain.APP_ENV=$ENVIRONMENT,nginx.host=$HOST,imageTag=${CI_COMMIT_SHA:0:8} --namespace symfony-dummy-project 145 | dependencies: [] 146 | only: 147 | - master 148 | 149 | deploy-staging: 150 | <<: *deployTemplate 151 | variables: 152 | ENVIRONMENT: stage 153 | CLUSTER_NAME: $CLUSTER_NAME_STAGE 154 | CLUSTER_ZONE: $CLUSTER_ZONE_STAGE 155 | HOST: $HOST_STAGE 156 | stage: stage-deployment 157 | after_script: 158 | # Wait for the rollout to complete 159 | - sh ci/wait-for-rollout symfony-dummy-project 160 | 161 | deploy-production: 162 | <<: *deployTemplate 163 | variables: 164 | ENVIRONMENT: prod 165 | CLUSTER_NAME: $CLUSTER_NAME_PROD 166 | CLUSTER_ZONE: $CLUSTER_ZONE_PROD 167 | HOST: $HOST_PROD 168 | stage: production-deployment 169 | when: manual 170 | 171 | end-to-end-test: 172 | stage: end-to-end-tests 173 | image: 174 | name: postman/newman:alpine 175 | entrypoint: [""] 176 | script: 177 | - newman run -e tests/EndToEnd/env.stage.json tests/EndToEnd/collection.json 178 | dependencies: [] 179 | only: 180 | - master 181 | 182 | .rollback-template: &rollbackTemplate 183 | image: babenkoivan/helm-gke 184 | before_script: 185 | # Fetch cluster credentials 186 | - sh ci/fetch-cluster-credentials $SERVICE_ACCOUNT_KEY $CLUSTER_NAME $CLUSTER_ZONE 187 | script: 188 | - helm rollback symfony-dummy-project 0 189 | dependencies: [] 190 | when: manual 191 | only: 192 | - master 193 | 194 | rollback-staging: 195 | <<: *rollbackTemplate 196 | variables: 197 | CLUSTER_NAME: $CLUSTER_NAME_STAGE 198 | CLUSTER_ZONE: $CLUSTER_ZONE_STAGE 199 | stage: stage-deployment 200 | 201 | rollback-production: 202 | <<: *rollbackTemplate 203 | variables: 204 | CLUSTER_NAME: $CLUSTER_NAME_PROD 205 | CLUSTER_ZONE: $CLUSTER_ZONE_PROD 206 | stage: production-deployment 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Symfony Dummy Project 2 | 3 | This project was created for **Let's Migrate Symfony Project to Kubernetes!** series: 4 | * [Part 1: Containerizing the Application](https://itnext.io/containerizing-symfony-application-a2a5a3bd5edc) 5 | * [Part 2: Publishing the Application with Helm](https://itnext.io/publishing-symfony-application-with-helm-ecb525b34289) 6 | * [Part 3: Testing the Application](https://medium.com/@babenko.i.a/testing-symfony-application-d02317d4018a) 7 | * [Part 4: Building a Continuous Delivery Pipeline](https://medium.com/@babenko.i.a/building-continuous-delivery-pipeline-2cc05e213935) 8 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 19 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 20 | } 21 | 22 | if ($input->hasParameterOption('--no-debug', true)) { 23 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 24 | } 25 | 26 | require dirname(__DIR__).'/config/bootstrap.php'; 27 | 28 | if ($_SERVER['APP_DEBUG']) { 29 | umask(0000); 30 | 31 | if (class_exists(Debug::class)) { 32 | Debug::enable(); 33 | } 34 | } 35 | 36 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 37 | $application = new Application($kernel); 38 | $application->run($input); 39 | -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =1.2) 9 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php')) { 10 | $_SERVER += $env; 11 | $_ENV += $env; 12 | } elseif (!class_exists(Dotenv::class)) { 13 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 14 | } else { 15 | // load all the .env files 16 | (new Dotenv())->loadEnv(dirname(__DIR__).'/.env'); 17 | } 18 | 19 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 20 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 21 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 22 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 8 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 9 | Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], 10 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 11 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 12 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 13 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], 14 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 15 | Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true], 16 | ]; 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/packages/dev/easy_log_handler.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | EasyCorp\EasyLog\EasyLogHandler: 3 | public: false 4 | arguments: ['%kernel.logs_dir%/%kernel.environment%.log'] 5 | 6 | #// FIXME: How to add this configuration automatically without messing up with the monolog configuration? 7 | #monolog: 8 | # handlers: 9 | # buffered: 10 | # type: buffer 11 | # handler: easylog 12 | # channels: ['!event'] 13 | # level: debug 14 | # easylog: 15 | # type: service 16 | # id: EasyCorp\EasyLog\EasyLogHandler 17 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | # uncomment to get logging in your browser 9 | # you may have to allow bigger header sizes in your Web server configuration 10 | #firephp: 11 | # type: firephp 12 | # level: info 13 | #chromephp: 14 | # type: chromephp 15 | # level: info 16 | console: 17 | type: console 18 | process_psr_3_messages: false 19 | channels: ["!event", "!doctrine", "!console"] 20 | -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | # Adds a fallback DATABASE_URL if the env var is not set. 3 | # This allows you to run cache:warmup even if your 4 | # environment variables are not available yet. 5 | # You should not need to change this value. 6 | env(DATABASE_URL): '' 7 | 8 | doctrine: 9 | dbal: 10 | # configure these for your database server 11 | driver: 'pdo_mysql' 12 | server_version: '%env(MYSQL_VERSION)%' 13 | charset: utf8mb4 14 | default_table_options: 15 | charset: utf8mb4 16 | collate: utf8mb4_unicode_ci 17 | 18 | url: '%env(resolve:DATABASE_URL)%' 19 | orm: 20 | auto_generate_proxy_classes: true 21 | naming_strategy: doctrine.orm.naming_strategy.underscore 22 | auto_mapping: true 23 | mappings: 24 | App: 25 | is_bundle: false 26 | type: annotation 27 | dir: '%kernel.project_dir%/src/Entity' 28 | prefix: 'App\Entity' 29 | alias: App 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #default_locale: en 4 | #csrf_protection: true 5 | #http_method_override: true 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: ~ 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | 14 | #esi: true 15 | #fragments: true 16 | php_errors: 17 | log: true 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_404s: 8 | # regex: exclude all 404 errors from the logs 9 | - ^/ 10 | nested: 11 | type: stream 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 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | utf8: true 5 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers 3 | providers: 4 | in_memory: { memory: ~ } 5 | firewalls: 6 | dev: 7 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 8 | security: false 9 | main: 10 | anonymous: true 11 | 12 | # activate different ways to authenticate 13 | 14 | # http_basic: true 15 | # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate 16 | 17 | # form_login: true 18 | # https://symfony.com/doc/current/security/form_login_setup.html 19 | 20 | # Easy way to control access for large sections of your site 21 | # Note: Only the *first* access control that matches will be used 22 | access_control: 23 | # - { path: ^/admin, roles: ROLE_ADMIN } 24 | # - { path: ^/profile, roles: ROLE_USER } 25 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | url: '%env(MAILER_URL)%' 3 | spool: { type: 'memory' } 4 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | -------------------------------------------------------------------------------- /config/packages/test/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/test/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | disable_delivery: true 3 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: '%locale%' 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - '%locale%' 7 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | form_themes: ['bootstrap_4_layout.html.twig'] 6 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | #index: 2 | # path: / 3 | # controller: App\Controller\DefaultController::index 4 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: ../../src/Controller/ 3 | type: annotation 4 | -------------------------------------------------------------------------------- /config/routes/dev/twig.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@TwigBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | nginx: 5 | build: 6 | args: 7 | VERSION: $NGINX_VERSION 8 | context: . 9 | dockerfile: ./docker/nginx/Dockerfile 10 | target: dev 11 | volumes: 12 | - ./assets:/app/public 13 | depends_on: 14 | - php-fpm 15 | ports: 16 | - 8000:80 17 | php-fpm: 18 | build: 19 | args: 20 | VERSION: $PHP_VERSION 21 | context: . 22 | dockerfile: ./docker/php-fpm/Dockerfile 23 | target: dev 24 | volumes: 25 | - .:/app 26 | command: sh -c 'composer install --no-interaction --optimize-autoloader && php-fpm' 27 | depends_on: 28 | - mysql 29 | mysql: 30 | build: 31 | args: 32 | VERSION: $MYSQL_VERSION 33 | context: ./docker/mysql 34 | environment: 35 | MYSQL_DATABASE: $MYSQL_DATABASE 36 | MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD 37 | volumes: 38 | - ./docker/mysql/data:/var/lib/mysql 39 | 40 | -------------------------------------------------------------------------------- /docker/helm-gke/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM google/cloud-sdk:alpine 2 | 3 | # Install system dependencies 4 | RUN apk add --update --no-cache openssl 5 | 6 | # Install kubectl 7 | RUN gcloud components install --quiet kubectl 8 | 9 | # Install helm 10 | ARG HELM_VERSION 11 | RUN curl https://raw.githubusercontent.com/helm/helm/master/scripts/get > ~/get_helm.sh && \ 12 | chmod +x ~/get_helm.sh && \ 13 | if [ -z $HELM_VERSION ]; then ~/get_helm.sh; else ~/get_helm.sh -v $HELM_VERSION; fi && \ 14 | rm ~/get_helm.sh 15 | -------------------------------------------------------------------------------- /docker/mysql/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VERSION 2 | 3 | FROM mysql:${VERSION} as prod 4 | 5 | # Copy sql dump to initialize tables on stratup 6 | COPY init /docker-entrypoint-initdb.d 7 | -------------------------------------------------------------------------------- /docker/mysql/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babenkoivan/symfony-dummy-project/92123cd2e9644f822a8d5dec2c4a96c081971acc/docker/mysql/data/.gitkeep -------------------------------------------------------------------------------- /docker/mysql/init/products.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `products` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | `name` varchar(255) NOT NULL DEFAULT '', 4 | `price` float NOT NULL, 5 | PRIMARY KEY (`id`) 6 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 7 | 8 | LOCK TABLES `products` WRITE; 9 | 10 | INSERT INTO `products` (`id`, `name`, `price`) 11 | VALUES 12 | (1,'Product 1',100), 13 | (2,'Product 2',200), 14 | (3,'Product 3',300); 15 | 16 | UNLOCK TABLES; 17 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VERSION 2 | 3 | # Dev image 4 | FROM nginx:${VERSION}-alpine as dev 5 | 6 | # Copy nginx config 7 | COPY ./docker/nginx/default.conf /etc/nginx/conf.d/default.conf 8 | 9 | # Prod image 10 | FROM dev as prod 11 | 12 | # Copy assets 13 | COPY ./assets /app/public 14 | -------------------------------------------------------------------------------- /docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | root /app/public; 3 | 4 | location / { 5 | try_files $uri /index.php$is_args$args; 6 | } 7 | 8 | location ~ ^/index\.php(/|$) { 9 | fastcgi_pass php-fpm:9000; 10 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 11 | include fastcgi_params; 12 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 13 | fastcgi_param DOCUMENT_ROOT $document_root; 14 | internal; 15 | } 16 | 17 | location ~ \.php$ { 18 | return 404; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docker/php-fpm/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VERSION 2 | 3 | # Dev image 4 | FROM php:${VERSION}-fpm-alpine AS dev 5 | 6 | ## Install system dependencies 7 | RUN apk update && \ 8 | apk add --no-cache --virtual dev-deps git autoconf gcc g++ make && \ 9 | apk add --no-cache zlib-dev libzip-dev 10 | 11 | ## Install php extensions 12 | RUN pecl install xdebug && \ 13 | docker-php-ext-enable xdebug && \ 14 | docker-php-ext-install pdo_mysql zip 15 | 16 | ## Copy php default configuration 17 | COPY ./docker/php-fpm/default.ini /usr/local/etc/php/conf.d/default.ini 18 | 19 | ENV APP_ENV=dev 20 | WORKDIR /app 21 | 22 | ## Install composer 23 | RUN wget https://getcomposer.org/installer && \ 24 | php installer --install-dir=/usr/local/bin/ --filename=composer && \ 25 | rm installer && \ 26 | composer global require hirak/prestissimo 27 | 28 | # Test image 29 | FROM dev AS test 30 | 31 | WORKDIR /app 32 | 33 | ## Copy project files to workdir 34 | COPY . . 35 | 36 | ## Install application dependencies 37 | RUN composer install --no-interaction --optimize-autoloader 38 | 39 | ## Change files owner to php-fpm default user 40 | RUN chown -R www-data:www-data . 41 | 42 | # Prod image 43 | FROM test AS prod 44 | 45 | ENV APP_ENV=prod 46 | WORKDIR /app 47 | 48 | ## Remove dev dependencies 49 | RUN composer install --no-dev --no-interaction --optimize-autoloader 50 | 51 | ## Disable xdebug on production 52 | RUN rm /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini 53 | 54 | ## Cleanup 55 | RUN apk del dev-deps && \ 56 | composer global remove hirak/prestissimo && \ 57 | rm /usr/local/bin/composer 58 | 59 | -------------------------------------------------------------------------------- /docker/php-fpm/default.ini: -------------------------------------------------------------------------------- 1 | memory_limit = 256M 2 | error_reporting = E_ERROR 3 | -------------------------------------------------------------------------------- /helm/.helmignore: -------------------------------------------------------------------------------- 1 | # OS specific 2 | .DS_Store 3 | 4 | # Common backup files 5 | *.swp 6 | *.bak 7 | *.tmp 8 | *~ 9 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Symfony dummy project 4 | name: symfony-dummy-project 5 | version: 1.0.0 6 | -------------------------------------------------------------------------------- /helm/templates/deployment.tpl: -------------------------------------------------------------------------------- 1 | {{- define "deployment.template" -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ .service.name | quote }} 6 | spec: 7 | replicas: {{ .service.replicas | default 1 }} 8 | selector: 9 | matchLabels: 10 | service: {{ .service.name | quote }} 11 | template: 12 | metadata: 13 | labels: 14 | service: {{ .service.name | quote }} 15 | spec: 16 | containers: 17 | - name: {{ .service.name | quote }} 18 | image: {{ printf "%s:%s" .service.image .imageTag | quote }} 19 | imagePullPolicy: {{ .service.pullPolicy | default "IfNotPresent" | quote }} 20 | resources: 21 | {{ toYaml .resources | trim | indent 12 }} 22 | {{- if (.service.probes) }} 23 | livenessProbe: 24 | httpGet: 25 | path: {{ .service.probes.liveness | quote }} 26 | port: {{ .service.port }} 27 | readinessProbe: 28 | httpGet: 29 | path: {{ .service.probes.readiness | quote }} 30 | port: {{ .service.port }} 31 | {{- end }} 32 | {{- if (.service.env) }} 33 | env: 34 | {{- if (.service.env.plain) }} 35 | {{- range $envName, $envValue := .service.env.plain }} 36 | - name: {{ $envName | quote }} 37 | value: {{ $envValue }} 38 | {{- end }} 39 | {{- end }} 40 | {{- if (.service.env.secret) }} 41 | {{- range $envName, $secret := .service.env.secret }} 42 | - name: {{ $envName | quote }} 43 | valueFrom: 44 | secretKeyRef: 45 | name: {{ $secret.name | quote }} 46 | key: {{ $secret.key | quote }} 47 | {{- end }} 48 | {{- end }} 49 | {{- end }} 50 | {{- end -}} 51 | -------------------------------------------------------------------------------- /helm/templates/ingress.tpl: -------------------------------------------------------------------------------- 1 | {{- define "ingress.template" -}} 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: {{ .service.name | quote }} 6 | spec: 7 | backend: 8 | serviceName: {{ .service.name | quote }} 9 | servicePort: {{ .service.port }} 10 | {{- end -}} 11 | -------------------------------------------------------------------------------- /helm/templates/mysql.deployment.yaml: -------------------------------------------------------------------------------- 1 | {{ include "deployment.template" (dict "service" .Values.mysql "imageTag" .Values.imageTag "resources" .Values.resources) }} 2 | -------------------------------------------------------------------------------- /helm/templates/mysql.service.yaml: -------------------------------------------------------------------------------- 1 | {{ include "service.template" (dict "service" .Values.mysql) }} 2 | -------------------------------------------------------------------------------- /helm/templates/nginx.deployment.yaml: -------------------------------------------------------------------------------- 1 | {{ include "deployment.template" (dict "service" .Values.nginx "imageTag" .Values.imageTag "resources" .Values.resources) }} 2 | -------------------------------------------------------------------------------- /helm/templates/nginx.ingress.yaml: -------------------------------------------------------------------------------- 1 | {{ include "ingress.template" (dict "service" .Values.nginx) }} 2 | -------------------------------------------------------------------------------- /helm/templates/nginx.service.yaml: -------------------------------------------------------------------------------- 1 | {{ include "service.template" (dict "service" .Values.nginx) }} 2 | -------------------------------------------------------------------------------- /helm/templates/php-fpm.deployment.yaml: -------------------------------------------------------------------------------- 1 | {{ include "deployment.template" (dict "service" .Values.phpfpm "imageTag" .Values.imageTag "resources" .Values.resources) }} 2 | -------------------------------------------------------------------------------- /helm/templates/php-fpm.service.yaml: -------------------------------------------------------------------------------- 1 | {{ include "service.template" (dict "service" .Values.phpfpm) }} 2 | -------------------------------------------------------------------------------- /helm/templates/service.tpl: -------------------------------------------------------------------------------- 1 | {{- define "service.template" -}} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ .service.name | quote }} 6 | spec: 7 | type: NodePort 8 | ports: 9 | - port: {{ .service.port }} 10 | selector: 11 | service: {{ .service.name | quote }} 12 | {{- end -}} 13 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | limits: 3 | cpu: 2 4 | memory: 1024Mi 5 | requests: 6 | cpu: 0.25 7 | memory: 256Mi 8 | 9 | mysql: 10 | name: "mysql" 11 | image: "babenkoivan/symfony-dummy-project-mysql" 12 | port: 3306 13 | env: 14 | secret: 15 | MYSQL_DATABASE: 16 | name: "database-secret" 17 | key: "database" 18 | MYSQL_ROOT_PASSWORD: 19 | name: "database-secret" 20 | key: "password" 21 | 22 | nginx: 23 | name: "nginx" 24 | image: "babenkoivan/symfony-dummy-project-nginx" 25 | port: 80 26 | probes: 27 | liveness: /healthz 28 | readiness: /healthz 29 | 30 | phpfpm: 31 | name: "php-fpm" 32 | image: "babenkoivan/symfony-dummy-project-php-fpm" 33 | port: 9000 34 | env: 35 | secret: 36 | DATABASE_URL: 37 | name: "database-secret" 38 | key: "url" 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | tests/Unit 19 | 20 | 21 | tests/Integration 22 | 23 | 24 | 25 | 26 | 27 | src 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 26 | $response->send(); 27 | $kernel->terminate($request, $response); 28 | -------------------------------------------------------------------------------- /src/Controller/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babenkoivan/symfony-dummy-project/92123cd2e9644f822a8d5dec2c4a96c081971acc/src/Controller/.gitignore -------------------------------------------------------------------------------- /src/Controller/HealthController.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 24 | } 25 | 26 | /** 27 | * @Route("/healthz") 28 | * @Route("/") 29 | * @return Response 30 | */ 31 | public function status(): Response 32 | { 33 | if ($this->connection->connect()) { 34 | return new Response('Up'); 35 | } else { 36 | return new Response('Down: database connection error', Response::HTTP_INTERNAL_SERVER_ERROR); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controller/ProductController.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 27 | } 28 | 29 | /** 30 | * @Route("/products/{id}", name="product_show", methods={"GET"}, requirements={"id"="\d+"}) 31 | * @Template(vars={"product"}) 32 | * @param Product $product 33 | */ 34 | public function show(Product $product): void 35 | { 36 | 37 | } 38 | 39 | /** 40 | * @Route("/products/add", name="product_add", methods={"GET","POST"}) 41 | * @Template() 42 | * @param Request $request 43 | * @return array|Response 44 | */ 45 | public function add(Request $request) 46 | { 47 | $form = $this->createForm(ProductAddType::class); 48 | $form->handleRequest($request); 49 | 50 | if ($form->isSubmitted() && $form->isValid()) { 51 | $product = $form->getData(); 52 | 53 | $this->entityManager->persist($product); 54 | $this->entityManager->flush(); 55 | 56 | return $this->redirectToRoute('product_show', ['id' => $product->getId()]); 57 | } 58 | 59 | return ['formView' => $form->createView()]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Entity/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babenkoivan/symfony-dummy-project/92123cd2e9644f822a8d5dec2c4a96c081971acc/src/Entity/.gitignore -------------------------------------------------------------------------------- /src/Entity/Product.php: -------------------------------------------------------------------------------- 1 | id; 36 | } 37 | 38 | public function getName(): ?string 39 | { 40 | return $this->name; 41 | } 42 | 43 | public function setName(string $name): self 44 | { 45 | $this->name = $name; 46 | 47 | return $this; 48 | } 49 | 50 | public function getPrice(): ?float 51 | { 52 | return $this->price; 53 | } 54 | 55 | public function setPrice(float $price): self 56 | { 57 | $this->price = $price; 58 | 59 | return $this; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Form/ProductAddType.php: -------------------------------------------------------------------------------- 1 | add('name', TextType::class) 23 | ->add('price', MoneyType::class, ['currency' => 'USD']) 24 | ->add('save', SubmitType::class); 25 | } 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | public function configureOptions(OptionsResolver $resolver) 31 | { 32 | $resolver->setDefaults([ 33 | 'data_class' => Product::class, 34 | 'csrf_token_id' => 'product_add_form' 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | getProjectDir().'/config/bundles.php'; 21 | foreach ($contents as $class => $envs) { 22 | if ($envs[$this->environment] ?? $envs['all'] ?? false) { 23 | yield new $class(); 24 | } 25 | } 26 | } 27 | 28 | public function getProjectDir(): string 29 | { 30 | return \dirname(__DIR__); 31 | } 32 | 33 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void 34 | { 35 | $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); 36 | $container->setParameter('container.dumper.inline_class_loader', true); 37 | $confDir = $this->getProjectDir().'/config'; 38 | 39 | $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); 40 | $loader->load($confDir.'/{packages}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); 41 | $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); 42 | $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); 43 | } 44 | 45 | protected function configureRoutes(RouteCollectionBuilder $routes): void 46 | { 47 | $confDir = $this->getProjectDir().'/config'; 48 | 49 | $routes->import($confDir.'/{routes}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); 50 | $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); 51 | $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Migrations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babenkoivan/symfony-dummy-project/92123cd2e9644f822a8d5dec2c4a96c081971acc/src/Migrations/.gitignore -------------------------------------------------------------------------------- /src/Repository/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babenkoivan/symfony-dummy-project/92123cd2e9644f822a8d5dec2c4a96c081971acc/src/Repository/.gitignore -------------------------------------------------------------------------------- /src/Repository/ProductRepository.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | {% block stylesheets %}{% endblock %} 8 | 9 | 10 |
11 | {% block body %}{% endblock %} 12 |
13 | {% block javascripts %}{% endblock %} 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/product/add.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %} New product {% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 |
9 |
10 | New product 11 |
12 |
13 | {{ form(formView) }} 14 |
15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /templates/product/show.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ product.name }}{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 |
9 |
10 | {{ product.name }} 11 |
12 |
13 |

Only today! Buy for {{ product.price }}$!

14 |
15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babenkoivan/symfony-dummy-project/92123cd2e9644f822a8d5dec2c4a96c081971acc/tests/.gitignore -------------------------------------------------------------------------------- /tests/EndToEnd/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "7cc6ed33-9d60-4731-aa38-e0b553e76995", 4 | "name": "Symfony Dummy Project", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Product ", 10 | "item": [ 11 | { 12 | "name": "Add", 13 | "item": [ 14 | { 15 | "name": "Submit invalid product form", 16 | "event": [ 17 | { 18 | "listen": "prerequest", 19 | "script": { 20 | "id": "9e1b255c-aac2-481f-a252-9f93a77b6058", 21 | "exec": [ 22 | "pm.variables.set(\"productName\", \"te\");", 23 | "pm.variables.set(\"productPrice\", 0);" 24 | ], 25 | "type": "text/javascript" 26 | } 27 | }, 28 | { 29 | "listen": "test", 30 | "script": { 31 | "id": "e1317b75-52fc-4068-b27c-9cd37ae3c9ef", 32 | "exec": [ 33 | "let $ = cheerio.load(pm.response.text());", 34 | "", 35 | "pm.test(\"Status code is 200\", function () {", 36 | " pm.response.to.have.status(200);", 37 | "});", 38 | "", 39 | "pm.test(\"Invalid name message is displayed\", function () {", 40 | " let errorMessage = $('label[for=\"product_add_name\"] .form-error-message').text().trim();", 41 | " pm.expect(errorMessage).to.equal(\"This value is too short. It should have 4 characters or more.\");", 42 | "});", 43 | "", 44 | "pm.test(\"Invalid price message is displayed\", function () {", 45 | " let errorMessage = $('label[for=\"product_add_price\"] .form-error-message').text().trim();", 46 | " pm.expect(errorMessage).to.include(\"This value should be greater than \\\"0\\\".\");", 47 | "});" 48 | ], 49 | "type": "text/javascript" 50 | } 51 | } 52 | ], 53 | "request": { 54 | "method": "POST", 55 | "header": [], 56 | "body": { 57 | "mode": "formdata", 58 | "formdata": [ 59 | { 60 | "key": "product_add[name]", 61 | "value": "{{productName}}", 62 | "type": "text" 63 | }, 64 | { 65 | "key": "product_add[price]", 66 | "value": "{{productPrice}}", 67 | "type": "text" 68 | }, 69 | { 70 | "key": "product_add[_token]", 71 | "value": "{{csrfToken}}", 72 | "type": "text" 73 | } 74 | ] 75 | }, 76 | "url": { 77 | "raw": "{{baseUrl}}/products/add", 78 | "host": [ 79 | "{{baseUrl}}" 80 | ], 81 | "path": [ 82 | "products", 83 | "add" 84 | ] 85 | } 86 | }, 87 | "response": [] 88 | }, 89 | { 90 | "name": "Submit valid product form", 91 | "event": [ 92 | { 93 | "listen": "prerequest", 94 | "script": { 95 | "id": "9e1b255c-aac2-481f-a252-9f93a77b6058", 96 | "exec": [ 97 | "pm.variables.set(\"productName\", \"test\");", 98 | "pm.variables.set(\"productPrice\", 1000);" 99 | ], 100 | "type": "text/javascript" 101 | } 102 | }, 103 | { 104 | "listen": "test", 105 | "script": { 106 | "id": "898250ee-cc1b-4666-814d-a218e4c5df18", 107 | "exec": [ 108 | "let $ = cheerio.load(pm.response.text());", 109 | "let productName = pm.variables.get(\"productName\");", 110 | "let productPrice = pm.variables.get(\"productPrice\");", 111 | "", 112 | "pm.test(\"Status code is 200\", function () {", 113 | " pm.response.to.have.status(200);", 114 | "});", 115 | "", 116 | "pm.test(\"New product is named as \" + productName, function () {", 117 | " let name = $('.card-header').text().trim();", 118 | " pm.expect(name).to.equal(productName);", 119 | "});", 120 | "", 121 | "pm.test(\"New product has price \" + productPrice, function () {", 122 | " let price = $('.card-text strong').text().trim();", 123 | " pm.expect(price).to.equal(productPrice + \"$\");", 124 | "});" 125 | ], 126 | "type": "text/javascript" 127 | } 128 | } 129 | ], 130 | "request": { 131 | "method": "POST", 132 | "header": [], 133 | "body": { 134 | "mode": "formdata", 135 | "formdata": [ 136 | { 137 | "key": "product_add[name]", 138 | "value": "{{productName}}", 139 | "type": "text" 140 | }, 141 | { 142 | "key": "product_add[price]", 143 | "value": "{{productPrice}}", 144 | "type": "text" 145 | }, 146 | { 147 | "key": "product_add[_token]", 148 | "value": "{{csrfToken}}", 149 | "type": "text" 150 | } 151 | ] 152 | }, 153 | "url": { 154 | "raw": "{{baseUrl}}/products/add", 155 | "host": [ 156 | "{{baseUrl}}" 157 | ], 158 | "path": [ 159 | "products", 160 | "add" 161 | ] 162 | } 163 | }, 164 | "response": [] 165 | } 166 | ], 167 | "event": [ 168 | { 169 | "listen": "prerequest", 170 | "script": { 171 | "id": "0ca26a07-7702-442c-9450-b0e9576d1aef", 172 | "type": "text/javascript", 173 | "exec": [ 174 | "const baseUrl = pm.variables.get(\"baseUrl\");", 175 | "", 176 | "pm.sendRequest(baseUrl + \"/products/add\", function (err, response) {", 177 | " let $ = cheerio.load(response.text());", 178 | " let csrfToken = $('#product_add__token').val();", 179 | " ", 180 | " pm.variables.set(\"csrfToken\", csrfToken);", 181 | "});" 182 | ] 183 | } 184 | }, 185 | { 186 | "listen": "test", 187 | "script": { 188 | "id": "2beac3f8-9a17-4b54-bee7-e904afcc7cf9", 189 | "type": "text/javascript", 190 | "exec": [ 191 | "" 192 | ] 193 | } 194 | } 195 | ], 196 | "_postman_isSubFolder": true 197 | } 198 | ] 199 | } 200 | ], 201 | "event": [ 202 | { 203 | "listen": "prerequest", 204 | "script": { 205 | "id": "a6965b47-dc61-4a7b-933a-8c82b8342a43", 206 | "type": "text/javascript", 207 | "exec": [ 208 | "" 209 | ] 210 | } 211 | }, 212 | { 213 | "listen": "test", 214 | "script": { 215 | "id": "460355cb-46cd-4cf5-85ab-a80cc27f4ca4", 216 | "type": "text/javascript", 217 | "exec": [ 218 | "" 219 | ] 220 | } 221 | } 222 | ] 223 | } -------------------------------------------------------------------------------- /tests/EndToEnd/env.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "579e876e-94e4-48c3-97bc-767d7014f00b", 3 | "name": "Symfony Dummy Project Dev", 4 | "values": [ 5 | { 6 | "key": "baseUrl", 7 | "value": "http://localhost:8000", 8 | "enabled": true 9 | } 10 | ], 11 | "_postman_variable_scope": "environment", 12 | "_postman_exported_at": "2019-06-14T13:18:21.213Z", 13 | "_postman_exported_using": "Postman/7.2.0" 14 | } -------------------------------------------------------------------------------- /tests/EndToEnd/env.stage.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3be60424-b3e2-4586-97a3-d7d6314b3703", 3 | "name": "Symfony Dummy Stage", 4 | "values": [ 5 | { 6 | "key": "baseUrl", 7 | "value": "stage.symfony-dummy-project.io", 8 | "enabled": true 9 | } 10 | ], 11 | "_postman_variable_scope": "environment", 12 | "_postman_exported_at": "2019-08-09T14:51:48.181Z", 13 | "_postman_exported_using": "Postman/7.2.2" 14 | } -------------------------------------------------------------------------------- /tests/Integration/Controller/ProductControllerTest.php: -------------------------------------------------------------------------------- 1 | createClient(); 24 | $repository = $this->entityManager->getRepository(Product::class); 25 | 26 | $httpClient->request(Request::METHOD_POST, $this->generateUrl('product_add'), [ 27 | 'product_add' => [ 28 | 'name' => $name, 29 | 'price' => $price, 30 | 'save' => '', 31 | '_token' => $this->generateCsrfToken('product_add_form')->getValue(), 32 | ], 33 | ]); 34 | 35 | $response = $httpClient->getResponse(); 36 | $product = $repository->findOneByName($name); 37 | 38 | $this->assertEquals($price, $product->getPrice()); 39 | $this->assertSame(302, $response->getStatusCode()); 40 | $this->assertSame($response->getTargetUrl(), $this->generateUrl('product_show', ['id' => $product->getId()])); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Support/CsrfTokenManager.php: -------------------------------------------------------------------------------- 1 | get('security.csrf.token_manager') 17 | ->getToken($tokenId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Support/Database.php: -------------------------------------------------------------------------------- 1 | entityManager = static::$container 20 | ->get('doctrine') 21 | ->getManager(); 22 | 23 | $this->entityManager->beginTransaction(); 24 | } 25 | 26 | /** 27 | * @after 28 | */ 29 | public function rollbackTransaction(): void 30 | { 31 | $this->entityManager->rollback(); 32 | $this->entityManager = null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Support/HttpClient.php: -------------------------------------------------------------------------------- 1 | get('test.client'); 16 | $client->setServerParameters($server); 17 | 18 | return $client; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Support/Kernel.php: -------------------------------------------------------------------------------- 1 | boot(); 31 | 32 | self::$container = self::$kernel->getContainer(); 33 | } 34 | 35 | /** 36 | * @afterClass 37 | */ 38 | public static function shutdownKernel(): void 39 | { 40 | static::$kernel->shutdown(); 41 | 42 | static::$kernel = null; 43 | static::$container = null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Support/Router.php: -------------------------------------------------------------------------------- 1 | get('router') 16 | ->generate($route, $parameters); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Unit/Controller/HealthControllerTest.php: -------------------------------------------------------------------------------- 1 | createConnectionMock(true); 17 | $controller = new HealthController($connectionMock); 18 | $response = $controller->status(); 19 | 20 | $this->assertSame(Response::HTTP_OK, $response->getStatusCode()); 21 | $this->assertContains('Up', $response->getContent()); 22 | } 23 | 24 | public function test_status_is_down_when_cant_connect_to_database(): void 25 | { 26 | $connectionMock = $this->createConnectionMock(false); 27 | $controller = new HealthController($connectionMock); 28 | $response = $controller->status(); 29 | 30 | $this->assertSame(Response::HTTP_INTERNAL_SERVER_ERROR, $response->getStatusCode()); 31 | $this->assertContains('Down', $response->getContent()); 32 | } 33 | 34 | /** 35 | * @param bool $reachable 36 | * @return MockObject 37 | */ 38 | private function createConnectionMock(bool $reachable): MockObject 39 | { 40 | $connectionMock = $this->createMock(Connection::class); 41 | 42 | $connectionMock 43 | ->expects($this->once()) 44 | ->method('connect') 45 | ->willReturn($reachable); 46 | 47 | return $connectionMock; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Unit/Controller/ProductControllerTest.php: -------------------------------------------------------------------------------- 1 | makeProductAddRequest($name, $price); 51 | $entityManagerMock = $this->createMock(EntityManager::class); 52 | 53 | $entityManagerMock 54 | ->expects($this->never()) 55 | ->method('persist'); 56 | 57 | $entityManagerMock 58 | ->expects($this->never()) 59 | ->method('flush'); 60 | 61 | $controller = new ProductController($entityManagerMock); 62 | $controller->setContainer(static::$container); 63 | $result = $controller->add($request); 64 | 65 | $this->assertArrayHasKey('formView', $result); 66 | $this->assertInstanceOf(FormView::class, $result['formView']); 67 | } 68 | 69 | /** 70 | * @dataProvider validProductAddFormValues 71 | * @param string $name 72 | * @param float $price 73 | */ 74 | public function test_product_is_persisted_when_submitted_valid_form(string $name, float $price): void 75 | { 76 | $request = $this->makeProductAddRequest($name, $price); 77 | $entityManagerMock = $this->createMock(EntityManager::class); 78 | 79 | $product = (new Product()) 80 | ->setName($name) 81 | ->setPrice($price); 82 | 83 | $entityManagerMock 84 | ->expects($this->once()) 85 | ->method('persist') 86 | ->with($product) 87 | ->willReturnCallback(function ($product) { 88 | $productReflection = new ReflectionClass($product); 89 | $idPropertyReflection = $productReflection->getProperty('id'); 90 | $idPropertyReflection->setAccessible(true); 91 | $idPropertyReflection->setValue($product, rand()); 92 | }); 93 | 94 | $entityManagerMock 95 | ->expects($this->once()) 96 | ->method('flush'); 97 | 98 | $controller = new ProductController($entityManagerMock); 99 | $controller->setContainer(static::$container); 100 | $result = $controller->add($request); 101 | 102 | $this->assertInstanceOf(RedirectResponse::class, $result); 103 | } 104 | 105 | /** 106 | * @param string $name 107 | * @param float $price 108 | * @return Request 109 | */ 110 | private function makeProductAddRequest(string $name, float $price): Request 111 | { 112 | $request = new Request([], [ 113 | 'product_add' => [ 114 | 'name' => $name, 115 | 'price' => $price, 116 | 'save' => '', 117 | '_token' => $this->generateCsrfToken('product_add_form')->getValue(), 118 | ], 119 | ]); 120 | 121 | $request->setMethod(Request::METHOD_POST); 122 | 123 | return $request; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /translations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babenkoivan/symfony-dummy-project/92123cd2e9644f822a8d5dec2c4a96c081971acc/translations/.gitignore --------------------------------------------------------------------------------