├── .env.dist ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── dependabot.yml └── workflows │ ├── blog-api_build.yml │ ├── blog-api_dependency.yml │ ├── blog-api_static.yml │ ├── blog_build.yml │ ├── blog_dependency.yml │ └── blog_static.yml ├── .gitignore ├── .styleci.yml ├── README.md ├── ansible ├── .gitignore ├── Makefile ├── authorize.yml ├── certbot.yml ├── deploy.yml ├── hosts.yml.dist ├── roles │ ├── certbot │ │ └── tasks │ │ │ ├── generate_dhparam.yml │ │ │ ├── main.yml │ │ │ └── update_certificates.yml │ ├── deploy │ │ └── tasks │ │ │ ├── deploy.yml │ │ │ └── main.yml │ └── server │ │ ├── handlers │ │ └── main.yml │ │ └── tasks │ │ ├── create_user.yml │ │ ├── install_dependencies.yml │ │ ├── install_docker.yml │ │ ├── install_docker_sdk.yml │ │ ├── main.yml │ │ └── swap.yml └── server.yml ├── blog-api ├── .dockerignore ├── .editorconfig ├── .env.example ├── .env.test ├── .gitattributes ├── .gitignore ├── .phpunit-watcher.yml ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── autoload.php ├── codeception.yml ├── composer.json ├── composer.lock ├── config │ ├── .gitignore │ ├── common │ │ ├── bootstrap.php │ │ ├── di │ │ │ ├── cache.php │ │ │ ├── cycle.php │ │ │ ├── hydrator.php │ │ │ ├── logger.php │ │ │ ├── psr17.php │ │ │ ├── router.php │ │ │ ├── translator.php │ │ │ └── validator.php │ │ ├── params.php │ │ └── routes.php │ ├── console │ │ ├── commands.php │ │ └── params.php │ ├── environments │ │ ├── dev │ │ │ └── params.php │ │ ├── prod │ │ │ └── params.php │ │ └── test │ │ │ └── params.php │ └── web │ │ ├── di │ │ ├── application.php │ │ ├── data-response.php │ │ ├── error-handler.php │ │ ├── middleware-dispatcher.php │ │ └── user.php │ │ ├── events.php │ │ └── params.php ├── configuration.php ├── data │ ├── db │ │ └── database.db │ └── nginx │ │ └── default.conf ├── docker-compose.yml ├── docker │ ├── dev │ │ ├── nginx │ │ │ ├── Dockerfile │ │ │ └── nginx.conf │ │ └── php │ │ │ ├── Dockerfile │ │ │ ├── conf.d │ │ │ └── php.ini │ │ │ └── php-fpm.d │ │ │ └── www.conf │ ├── docker-entrypoint.sh │ └── prod │ │ ├── nginx │ │ ├── Dockerfile │ │ └── nginx.conf │ │ └── php │ │ ├── Dockerfile │ │ ├── conf.d │ │ └── php.ini │ │ └── php-fpm.d │ │ └── www.conf ├── infection.json.dist ├── phpunit.xml.dist ├── psalm.xml ├── public │ ├── .htaccess │ ├── assets │ │ └── .gitignore │ ├── favicon.ico │ ├── index.php │ └── robots.txt ├── resources │ └── messages │ │ ├── de │ │ └── app.php │ │ ├── en │ │ └── app.php │ │ └── ru │ │ └── app.php ├── runtime │ └── .gitignore ├── src │ ├── Auth │ │ ├── AuthController.php │ │ ├── AuthRequest.php │ │ └── AuthRequestErrorHandler.php │ ├── Blog │ │ ├── BlogController.php │ │ ├── BlogService.php │ │ ├── EditPostRequest.php │ │ ├── Post.php │ │ ├── PostBuilder.php │ │ ├── PostFormatter.php │ │ ├── PostRepository.php │ │ └── PostStatus.php │ ├── Dto │ │ └── ApiResponseData.php │ ├── Exception │ │ ├── ApplicationException.php │ │ ├── BadRequestException.php │ │ ├── MethodNotAllowedException.php │ │ ├── NotFoundException.php │ │ └── UnauthorisedException.php │ ├── Factory │ │ ├── ApiResponseDataFactory.php │ │ └── RestGroupFactory.php │ ├── Formatter │ │ ├── ApiResponseFormatter.php │ │ └── PaginatorFormatter.php │ ├── Handler │ │ └── NotFoundHandler.php │ ├── InfoController.php │ ├── Installer.php │ ├── Middleware │ │ └── ExceptionMiddleware.php │ ├── Queue │ │ ├── LoggingAuthorizationHandler.php │ │ └── UserLoggedInMessage.php │ ├── RestControllerTrait.php │ ├── User │ │ ├── User.php │ │ ├── UserController.php │ │ ├── UserFormatter.php │ │ ├── UserRepository.php │ │ ├── UserRequest.php │ │ └── UserService.php │ └── VersionProvider.php ├── tests │ ├── .gitkeep │ ├── Acceptance.suite.yml │ ├── Acceptance │ │ ├── AuthCest.php │ │ ├── BlogCest.php │ │ ├── SiteCest.php │ │ └── UserCest.php │ ├── Cli.suite.yml │ ├── Cli │ │ └── ConsoleCest.php │ ├── Functional.suite.yml │ ├── Functional │ │ └── IndexControllerTest.php │ ├── Support │ │ ├── AcceptanceTester.php │ │ ├── CliTester.php │ │ ├── Data │ │ │ ├── database.db │ │ │ └── dump.sql │ │ ├── FunctionalTester.php │ │ ├── Helper │ │ │ ├── Acceptance.php │ │ │ ├── Cli.php │ │ │ ├── Functional.php │ │ │ └── Unit.php │ │ ├── UnitTester.php │ │ └── _generated │ │ │ └── .gitignore │ ├── Unit.suite.yml │ └── Unit │ │ └── .gitkeep ├── yii └── yii.bat ├── blog ├── .dockerignore ├── .editorconfig ├── .env.example ├── .env.test ├── .gitattributes ├── .gitignore ├── .phpunit-watcher.yml ├── LICENSE.md ├── Makefile ├── README.md ├── autoload.php ├── codeception.yml ├── composer.json ├── composer.lock ├── config │ ├── .gitignore │ ├── common │ │ ├── bootstrap.php │ │ ├── di │ │ │ ├── cache.php │ │ │ ├── cycle.php │ │ │ ├── hydrator.php │ │ │ ├── logger.php │ │ │ ├── mailer.php │ │ │ ├── psr17.php │ │ │ ├── rbac.php │ │ │ ├── router.php │ │ │ ├── sentry.php │ │ │ ├── translator.php │ │ │ └── validator.php │ │ ├── params.php │ │ ├── rbac-rules.php │ │ └── routes │ │ │ ├── routes-backend.php │ │ │ └── routes.php │ ├── console │ │ ├── commands.php │ │ ├── di │ │ │ └── translator-extractor.php │ │ ├── events.php │ │ └── params.php │ ├── environments │ │ ├── dev │ │ │ └── params.php │ │ ├── prod │ │ │ └── params.php │ │ └── test │ │ │ └── params.php │ └── web │ │ ├── di │ │ ├── application.php │ │ ├── auth.php │ │ ├── comment-service.php │ │ ├── contact-mailer.php │ │ ├── middleware-dispatcher.php │ │ └── rate-limit.php │ │ ├── events.php │ │ ├── params.php │ │ └── widgets.php ├── configuration.php ├── dependency-checker.json ├── docker-compose.yml ├── docker │ ├── dev │ │ ├── nginx │ │ │ ├── Dockerfile │ │ │ └── nginx.conf │ │ └── php │ │ │ ├── Dockerfile │ │ │ ├── conf.d │ │ │ └── php.ini │ │ │ └── php-fpm.d │ │ │ └── www.conf │ ├── docker-entrypoint.sh │ └── prod │ │ ├── nginx │ │ ├── Dockerfile │ │ └── nginx.conf │ │ └── php │ │ ├── Dockerfile │ │ ├── conf.d │ │ └── php.ini │ │ └── php-fpm.d │ │ └── www.conf ├── package-lock.json ├── package.json ├── phpunit.xml.dist ├── psalm.xml ├── public │ ├── .htaccess │ ├── assets │ │ └── .gitignore │ ├── favicon.ico │ ├── index.php │ └── robots.txt ├── resources │ ├── asset │ │ ├── css │ │ │ └── site.css │ │ └── js │ │ │ └── app.js │ ├── backend │ │ └── views │ │ │ └── site │ │ │ └── index.php │ ├── mail │ │ └── layouts │ │ │ └── html.php │ ├── messages │ │ ├── de │ │ │ └── app.php │ │ ├── en │ │ │ └── app.php │ │ ├── id │ │ │ └── app.php │ │ ├── ru │ │ │ └── app.php │ │ └── sk │ │ │ └── app.php │ ├── rbac │ │ └── items.php │ └── views │ │ ├── auth │ │ └── login.php │ │ ├── blog │ │ ├── _archive.php │ │ ├── _topTags.php │ │ ├── archive │ │ │ ├── index.php │ │ │ ├── monthly-archive.php │ │ │ └── yearly-archive.php │ │ ├── comments │ │ │ ├── _comments.php │ │ │ └── index.php │ │ ├── index.php │ │ ├── post │ │ │ ├── __form.php │ │ │ └── index.php │ │ └── tag │ │ │ └── index.php │ │ ├── layout │ │ └── main.php │ │ ├── signup │ │ └── signup.php │ │ ├── site │ │ ├── 404.php │ │ └── index.php │ │ └── user │ │ ├── index.php │ │ └── profile.php ├── runtime │ └── .gitignore ├── src │ ├── Asset │ │ ├── AppAsset.php │ │ └── Bootstrap5IconsAsset.php │ ├── Auth │ │ ├── AuthService.php │ │ ├── Controller │ │ │ ├── AuthController.php │ │ │ └── SignupController.php │ │ ├── Form │ │ │ ├── LoginForm.php │ │ │ └── SignupForm.php │ │ ├── Identity.php │ │ └── IdentityRepository.php │ ├── Backend │ │ └── Controller │ │ │ └── SiteController.php │ ├── Blog │ │ ├── Archive │ │ │ ├── ArchiveController.php │ │ │ └── ArchiveRepository.php │ │ ├── BlogController.php │ │ ├── Comment │ │ │ ├── CommentRepository.php │ │ │ ├── CommentService.php │ │ │ └── Scope │ │ │ │ └── PublicScope.php │ │ ├── CommentController.php │ │ ├── Entity │ │ │ ├── Comment.php │ │ │ ├── Post.php │ │ │ ├── PostTag.php │ │ │ └── Tag.php │ │ ├── Post │ │ │ ├── PostController.php │ │ │ ├── PostForm.php │ │ │ ├── PostRepository.php │ │ │ ├── PostService.php │ │ │ └── Scope │ │ │ │ └── PublicScope.php │ │ ├── Tag │ │ │ ├── TagController.php │ │ │ └── TagRepository.php │ │ └── Widget │ │ │ └── PostCard.php │ ├── Command │ │ ├── Fixture │ │ │ ├── AddCommand.php │ │ │ └── SchemaClearCommand.php │ │ ├── Router │ │ │ └── ListCommand.php │ │ └── Translation │ │ │ └── TranslateCommand.php │ ├── Contact │ │ ├── ContactController.php │ │ ├── ContactForm.php │ │ ├── ContactMailer.php │ │ ├── mail │ │ │ ├── contact-email.php │ │ │ └── layouts │ │ │ │ └── html.php │ │ └── views │ │ │ └── contact │ │ │ └── form.php │ ├── Controller │ │ ├── Actions │ │ │ └── ApiInfo.php │ │ └── SiteController.php │ ├── Handler │ │ └── NotFoundHandler.php │ ├── Installer.php │ ├── Middleware │ │ ├── AccessChecker.php │ │ └── ApiDataWrapper.php │ ├── Service │ │ └── WebControllerService.php │ ├── Timer.php │ ├── User │ │ ├── Console │ │ │ ├── AssignRoleCommand.php │ │ │ └── CreateCommand.php │ │ ├── Controller │ │ │ ├── ApiUserController.php │ │ │ └── UserController.php │ │ ├── User.php │ │ ├── UserRepository.php │ │ └── UserService.php │ ├── ViewInjection │ │ ├── CommonViewInjection.php │ │ ├── LayoutViewInjection.php │ │ ├── LinkTagsViewInjection.php │ │ └── MetaTagsViewInjection.php │ └── Widget │ │ ├── FlashMessage.php │ │ ├── OffsetPagination.php │ │ └── PerformanceMetrics.php ├── tests │ ├── Acceptance.suite.yml │ ├── Acceptance │ │ ├── BlogPageCest.php │ │ ├── CommentPageCest.php │ │ ├── ContactPageCest.php │ │ ├── IndexPageCest.php │ │ ├── LoginAcceptanceCest.php │ │ ├── SignupAcceptanceCest.php │ │ └── UserPageCest.php │ ├── Cli.suite.yml │ ├── Cli │ │ └── ConsoleCest.php │ ├── Functional.suite.yml │ ├── Functional │ │ ├── ContactCest.php │ │ ├── EventListenerConfigurationTest.php │ │ └── IndexControllerTest.php │ ├── Support │ │ ├── AcceptanceTester.php │ │ ├── CliTester.php │ │ ├── FunctionalTester.php │ │ ├── Helper │ │ │ ├── Acceptance.php │ │ │ ├── Functional.php │ │ │ └── Unit.php │ │ ├── UnitTester.php │ │ └── _generated │ │ │ └── .gitignore │ ├── Unit.suite.yml │ └── Unit │ │ └── .gitkeep ├── yii └── yii.bat ├── demo ├── Dockerfile └── html │ └── index.html ├── docker-compose.override.yml ├── docker-compose.yml ├── gateway └── nginx │ ├── dev │ ├── Dockerfile │ └── templates │ │ └── http.conf.template │ └── prod │ ├── Dockerfile │ ├── backup │ ├── http.conf.template │ └── https.conf.template │ └── templates │ └── https.conf.template └── var ├── .gitignore └── ssl └── www └── .gitignore /.env.dist: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=yii-demo 2 | 3 | REGISTRY=localhost 4 | DOMAIN=yii-demo.localhost 5 | SUPPORT_EMAIL=team@yiiframework.com 6 | # Get short image tag git rev-parse --short HEAD 7 | IMAGE_TAG=00001 8 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | - [Yii goal and values](https://github.com/yiisoft/docs/blob/master/001-yii-values.md) 4 | - [Namespaces](https://github.com/yiisoft/docs/blob/master/004-namespaces.md) 5 | - [Git commit messages](https://github.com/yiisoft/docs/blob/master/006-git-commit-messages.md) 6 | - [Exceptions](https://github.com/yiisoft/docs/blob/master/007-exceptions.md) 7 | - [Interfaces](https://github.com/yiisoft/docs/blob/master/008-interfaces.md) 8 | 9 | # Getting started 10 | 11 | Since Yii 3 consists of many packages, we have a [special development tool](https://github.com/yiisoft/docs/blob/master/005-development-tool.md). 12 | 13 | 1. [Clone the repository](https://github.com/yiisoft/yii-dev-tool). 14 | 15 | 2. [Set up your own fork](https://github.com/yiisoft/yii-dev-tool#using-your-own-fork). 16 | 17 | 3. Now you are ready. Fork any package listed in `packages.php` and do `./yii-dev install username/package`. 18 | 19 | If you don't have any particular package in mind to start with: 20 | 21 | - [Check roadmap](https://github.com/yiisoft/docs/blob/master/003-roadmap.md). 22 | - Check package issues at github. Usually there are some. 23 | - Ask @samdark. 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: yiisoft 4 | github: [yiisoft] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### What steps will reproduce the problem? 4 | 5 | ### What is the expected result? 6 | 7 | ### What do you get instead? 8 | 9 | 10 | ### Additional info 11 | 12 | | Q | A 13 | | ---------------- | --- 14 | | Version | 1.0.? 15 | | PHP version | 16 | | Operating system | 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | ------------- | --- 3 | | Is bugfix? | ✔️/❌ 4 | | New feature? | ✔️/❌ 5 | | Breaks BC? | ✔️/❌ 6 | | Fixed issues | comma-separated list of tickets # fixed by the PR, if any 7 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Please use the [security issue form](https://www.yiiframework.com/security) to report to us any security issue you 4 | find in Yii. DO NOT use the issue tracker or discuss it in the public forum as it will cause more damage than help. 5 | 6 | Please note that as a non-commercial OpenSource project we are not able to pay bounties at the moment. 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions. 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | # Too noisy. See https://github.community/t/increase-if-necessary-for-github-actions-in-dependabot/179581 9 | open-pull-requests-limit: 0 10 | 11 | # Maintain dependencies for Composer 12 | - package-ecosystem: "composer" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | versioning-strategy: increase-if-necessary 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE & OS files 2 | .*.swp 3 | .DS_Store 4 | .buildpath 5 | .idea 6 | .project 7 | .settings 8 | Thumbs.db 9 | nbproject 10 | .env 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yii 3 Demo Code 2 | =============== 3 | 4 | This repository contains the code for the Yii 3 demo projects: 5 | 6 | Deployment 7 | ---------- 8 | 9 | ### Manually Renew SSL Cert 10 | 11 | docker-compose run certbot 12 | docker-compose restart gateway 13 | -------------------------------------------------------------------------------- /ansible/.gitignore: -------------------------------------------------------------------------------- 1 | hosts.yml 2 | -------------------------------------------------------------------------------- /ansible/Makefile: -------------------------------------------------------------------------------- 1 | server: 2 | python3 -m ansible playbook -i hosts.yml server.yml -vv 3 | 4 | deploy: 5 | python3 -m ansible playbook -i hosts.yml deploy.yml -vv 6 | 7 | authorize: 8 | python3 -m ansible playbook -i hosts.yml authorize.yml 9 | 10 | certbot: 11 | python3 -m ansible playbook -i hosts.yml certbot.yml 12 | -------------------------------------------------------------------------------- /ansible/authorize.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add authorized key 3 | hosts: all 4 | remote_user: root 5 | tasks: 6 | - name: Add user authorized key 7 | authorized_key: 8 | user: deploy 9 | key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" 10 | 11 | - name: Add deploy user to the sudoers 12 | copy: 13 | dest: "/etc/sudoers.d/deploy" 14 | content: "deploy ALL=(ALL) NOPASSWD: ALL" 15 | -------------------------------------------------------------------------------- /ansible/certbot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Renew certificates 3 | hosts: all 4 | remote_user: root 5 | roles: 6 | - certbot 7 | -------------------------------------------------------------------------------- /ansible/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Deploy services 3 | hosts: all 4 | remote_user: root 5 | roles: 6 | - deploy 7 | -------------------------------------------------------------------------------- /ansible/hosts.yml.dist: -------------------------------------------------------------------------------- 1 | all: 2 | hosts: 3 | demo.yiiframework.com: 4 | ansible_connection: ssh 5 | ansible_user: root 6 | ansible_host: 0.0.0.0 7 | ansible_port: 22 8 | -------------------------------------------------------------------------------- /ansible/roles/certbot/tasks/generate_dhparam.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create folder 3 | file: 4 | path: /home/deploy/demo.yiiframework.com/var/ssl/dhparam 5 | state: directory 6 | recurse: true 7 | 8 | - name: Generate dhparam 9 | command: openssl dhparam -out ./var/ssl/dhparam/dhparam-2048.pem 2048 10 | args: 11 | chdir: /home/deploy/demo.yiiframework.com 12 | creates: ./var/ssl/dhparam/dhparam-2048.pem -------------------------------------------------------------------------------- /ansible/roles/certbot/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - import_tasks: generate_dhparam.yml 2 | - import_tasks: update_certificates.yml 3 | -------------------------------------------------------------------------------- /ansible/roles/certbot/tasks/update_certificates.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Up docker services 3 | become: true 4 | become_user: deploy 5 | args: 6 | chdir: /home/deploy/demo.yiiframework.com 7 | shell: docker-compose -f docker-compose.yml up certbot 8 | register: output 9 | 10 | - debug: 11 | var: output.stdout_lines 12 | 13 | - name: Reload nginx service 14 | become: true 15 | become_user: deploy 16 | args: 17 | chdir: /home/deploy/demo.yiiframework.com 18 | shell: docker-compose -f docker-compose.yml exec gateway nginx -t && docker-compose -f docker-compose.yml exec gateway nginx -s reload 19 | 20 | # Crontab file location is /var/spool/cron/crontabs/deploy 21 | # Every 2nd month on 15th day of month 22 | # See https://crontab.guru/#0_0_15_*/2_* 23 | - name: Set periodic certificates update 24 | cron: 25 | name: certbot-renew 26 | user: deploy 27 | minute: '0' 28 | hour: '0' 29 | day: '15' 30 | month: '*/2' 31 | job: > 32 | /bin/bash -c " 33 | cd /home/deploy/demo.yiiframework.com && 34 | docker-compose -f docker-compose.yml up certbot && 35 | sleep 180 && 36 | docker-compose -f docker-compose.yml exec -T gateway nginx -t && 37 | docker-compose -f docker-compose.yml exec -T gateway nginx -s reload 38 | " 39 | -------------------------------------------------------------------------------- /ansible/roles/deploy/tasks/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Chown 3 | file: 4 | path: "{{ item }}" 5 | recurse: true 6 | owner: deploy 7 | group: deploy 8 | loop: 9 | - /home/deploy/demo.yiiframework.com/blog 10 | - /home/deploy/demo.yiiframework.com/blog-api 11 | 12 | - name: Run `docker-compose up` 13 | become: true 14 | become_user: deploy 15 | args: 16 | chdir: ~/demo.yiiframework.com 17 | shell: docker-compose -f docker-compose.yml up -d --build 18 | -------------------------------------------------------------------------------- /ansible/roles/deploy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - import_tasks: deploy.yml 2 | -------------------------------------------------------------------------------- /ansible/roles/server/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Start docker on system boot 3 | systemd: 4 | name: docker 5 | state: started 6 | enabled: yes 7 | -------------------------------------------------------------------------------- /ansible/roles/server/tasks/create_user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create deploy user 3 | vars: 4 | newpassword: "SomEP@$$w0rd" 5 | user: 6 | name: deploy 7 | password: "{{ newpassword }}" 8 | update_password: always 9 | groups: 10 | - docker 11 | - www-data 12 | state: present 13 | shell: /bin/bash 14 | 15 | -------------------------------------------------------------------------------- /ansible/roles/server/tasks/install_dependencies.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install `passlib` for Python 3 | pip: 4 | name: 5 | - passlib 6 | state: present 7 | 8 | - name: Install `composer` into system 9 | apt: 10 | name: 11 | - composer 12 | state: present -------------------------------------------------------------------------------- /ansible/roles/server/tasks/install_docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install `docker` dependencies 3 | apt: 4 | name: 5 | - apt-transport-https 6 | - ca-certificates 7 | - curl 8 | - software-properties-common 9 | state: present 10 | update_cache: yes 11 | 12 | - name: Add GPG key 13 | apt_key: 14 | url: https://download.docker.com/linux/debian/gpg 15 | state: present 16 | 17 | - name: Verify fingerprint 18 | apt_key: 19 | id: 0EBFCD88 20 | state: present 21 | 22 | - name: Set up the repository 23 | apt_repository: 24 | repo: deb [arch=amd64] https://download.docker.com/linux/debian stretch stable 25 | state: present 26 | filename: docker 27 | update_cache: yes 28 | 29 | - name: Install `docker` 30 | apt: 31 | name: docker-ce 32 | state: present 33 | update_cache: yes 34 | notify: Start docker on boot 35 | 36 | - name: Install `docker-compose` 37 | get_url: 38 | url: https://github.com/docker/compose/releases/download/v2.6.0/docker-compose-{{ ansible_system }}-{{ ansible_userspace_architecture }} 39 | dest: /usr/local/bin/docker-compose 40 | group: docker 41 | mode: 'u+x,g+x' 42 | 43 | - name: Set periodic Docker prune 44 | cron: 45 | name: docker-prune 46 | job: 'docker system prune -af --filter "until=$((30*24))h"' 47 | minute: '0' 48 | hour: '1' 49 | 50 | - name: Remove useless packages 51 | apt: 52 | autoclean: yes 53 | 54 | - name: Remove useless dependencies 55 | apt: 56 | autoremove: yes 57 | -------------------------------------------------------------------------------- /ansible/roles/server/tasks/install_docker_sdk.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Docker SDK dependencies 3 | apt: 4 | name: 5 | - python3-pip 6 | - python-setuptools 7 | - virtualenv 8 | state: present 9 | update_cache: yes 10 | 11 | - name: Install Docker SDK for Python 12 | pip: 13 | name: 14 | - docker 15 | state: present 16 | 17 | -------------------------------------------------------------------------------- /ansible/roles/server/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - import_tasks: swap.yml 2 | - import_tasks: install_dependencies.yml 3 | - import_tasks: install_docker.yml 4 | - import_tasks: install_docker_sdk.yml 5 | - import_tasks: create_user.yml 6 | -------------------------------------------------------------------------------- /ansible/roles/server/tasks/swap.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check whether swap is already enabled or not 3 | shell: cat /etc/sysctl.conf 4 | register: swap_enabled 5 | 6 | - block: 7 | - name: create swap file 8 | command: dd if=/dev/zero of=/swapfile bs=1G count=8 9 | 10 | - name: change permission type 11 | file: path=/swapfile mode=600 state=file 12 | 13 | - name: setup swap 14 | command: mkswap /swapfile 15 | 16 | - name: create swap 17 | command: swapon /swapfile 18 | 19 | - name: Add to fstab 20 | lineinfile: 21 | path: /etc/fstab 22 | regexp: "swapfile" 23 | line: "/swapfile none swap sw 0 0" 24 | state: present 25 | 26 | - name: start swap 27 | command: swapon -a 28 | 29 | - name: set swapiness 30 | sysctl: 31 | name: vm.swappiness 32 | value: "10" 33 | 34 | - name: set swapiness 35 | sysctl: 36 | name: vm.vfs_cache_pressure 37 | value: "50" 38 | 39 | when: swap_enabled.stdout.find('swappiness') == -1 -------------------------------------------------------------------------------- /ansible/server.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup server 3 | hosts: all 4 | remote_user: root 5 | roles: 6 | - server 7 | -------------------------------------------------------------------------------- /blog-api/.dockerignore: -------------------------------------------------------------------------------- 1 | runtime/ 2 | !runtime/.gitignore 3 | vendor/ 4 | -------------------------------------------------------------------------------- /blog-api/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | 13 | [*.php] 14 | ij_php_space_before_short_closure_left_parenthesis = false 15 | ij_php_space_after_type_cast = true 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | 20 | [*.yml] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /blog-api/.env.example: -------------------------------------------------------------------------------- 1 | YII_ENV= 2 | YII_DEBUG=true 3 | BASE_URL=/blog-api 4 | -------------------------------------------------------------------------------- /blog-api/.env.test: -------------------------------------------------------------------------------- 1 | YII_ENV=test 2 | YII_DEBUG=true 3 | BASE_URL=/ 4 | -------------------------------------------------------------------------------- /blog-api/.gitattributes: -------------------------------------------------------------------------------- 1 | # Autodetect text files 2 | * text=auto eol=lf 3 | 4 | # ...Unless the name matches the following overriding patterns 5 | 6 | # Definitively text files 7 | *.php text 8 | *.css text 9 | *.js text 10 | *.txt text 11 | *.md text 12 | *.xml text 13 | *.json text 14 | *.bat text 15 | *.sql text 16 | *.yml text 17 | 18 | # Ensure those won't be messed up with 19 | *.png binary 20 | *.jpg binary 21 | *.gif binary 22 | *.ttf binary 23 | 24 | # Avoid merge conflicts in CHANGELOG 25 | # https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ 26 | /CHANGELOG.md merge=union 27 | 28 | -------------------------------------------------------------------------------- /blog-api/.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | 21 | # Mac DS_Store Files 22 | .DS_Store 23 | 24 | # phpunit itself is not needed 25 | phpunit.phar 26 | # local phpunit config 27 | /phpunit.xml 28 | # phpunit cache 29 | .phpunit.result.cache 30 | 31 | # Codeception 32 | c3.php 33 | 34 | #tests 35 | tests/Support/Data/database.db_snapshot 36 | 37 | .env 38 | -------------------------------------------------------------------------------- /blog-api/.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /blog-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.0.0 under development 4 | 5 | - Initial release. 6 | -------------------------------------------------------------------------------- /blog-api/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /blog-api/Makefile: -------------------------------------------------------------------------------- 1 | init: composer-update up 2 | 3 | up: 4 | docker-compose up -d 5 | down: 6 | docker-compose down 7 | 8 | composer: 9 | docker-compose run php composer $(filter-out $@, $(MAKECMDGOALS)) 10 | composer-update: 11 | docker-compose run php composer update 12 | 13 | yii3: 14 | docker-compose run php ./yii $(filter-out $@, $(MAKECMDGOALS)) 15 | 16 | test: 17 | docker-compose run php ./vendor/bin/codecept run 18 | psalm: 19 | docker-compose run php ./vendor/bin/psalm 20 | -------------------------------------------------------------------------------- /blog-api/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Yii 4 | 5 |

Yii Framework API Demo Project

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/demo-api/v/stable.png)](https://packagist.org/packages/yiisoft/demo-api) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/demo-api/downloads.png)](https://packagist.org/packages/yiisoft/demo-api) 11 | [![Build status](https://github.com/yiisoft/demo-api/workflows/build/badge.svg)](https://github.com/yiisoft/demo-api/actions?query=workflow%3Abuild) 12 | [![static analysis](https://github.com/yiisoft/demo-api/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/demo-api/actions?query=workflow%3A%22static+analysis%22) 13 | 14 | API Demo application for Yii 3. 15 | 16 | ## Installation 17 | 18 | Install docker: 19 | 20 | ```bash 21 | docker-compose up -d 22 | ``` 23 | 24 | Enter into the container: 25 | 26 | ```bash 27 | docker exec -it yii-php bash 28 | ``` 29 | 30 | Install packages: 31 | 32 | ```bash 33 | composer install 34 | ``` 35 | 36 | Change ownership of the app directory to web group: 37 | 38 | ```bash 39 | chown -R :www-data . 40 | ``` 41 | 42 | Usually the application is available at . 43 | 44 | Authorization is performed via the `X-Api-Key` header. 45 | 46 | ## API documentation 47 | 48 | API documentation is available at `/docs`. It is built from OpenAPI attributes (`#[OA\ ... ]`). 49 | See [Swagger-PHP documentation](https://zircote.github.io/swagger-php/Getting-started.html#write-annotations) for details 50 | on how to annotate your code. 51 | 52 | ## Codeception testing 53 | 54 | ```php 55 | ./vendor/bin/codecept run 56 | ``` 57 | 58 | ## Static analysis 59 | 60 | The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis: 61 | 62 | ```php 63 | ./vendor/bin/psalm 64 | ``` 65 | -------------------------------------------------------------------------------- /blog-api/autoload.php: -------------------------------------------------------------------------------- 1 | load(); 11 | 12 | $_ENV['YII_ENV'] = empty($_ENV['YII_ENV']) ? null : (string) $_ENV['YII_ENV']; 13 | $_SERVER['YII_ENV'] = $_ENV['YII_ENV']; 14 | 15 | $_ENV['YII_DEBUG'] = filter_var( 16 | !empty($_ENV['YII_DEBUG']) ? $_ENV['YII_DEBUG'] : true, 17 | FILTER_VALIDATE_BOOLEAN, 18 | FILTER_NULL_ON_FAILURE 19 | ) ?? true; 20 | $_SERVER['YII_DEBUG'] = $_ENV['YII_DEBUG']; 21 | -------------------------------------------------------------------------------- /blog-api/codeception.yml: -------------------------------------------------------------------------------- 1 | namespace: App\Tests 2 | support_namespace: Support 3 | paths: 4 | tests: tests 5 | output: runtime/tests/_output 6 | data: tests/Support/Data 7 | support: tests/Support 8 | envs: tests/Support/Envs 9 | actor_suffix: Tester 10 | extensions: 11 | enabled: 12 | - Codeception\Extension\RunFailed 13 | settings: 14 | suite_class: \PHPUnit_Framework_TestSuite 15 | memory_limit: 1024M 16 | colors: true 17 | coverage: 18 | enabled: true 19 | whitelist: 20 | include: 21 | - src/* 22 | params: 23 | - .env 24 | - .env.test 25 | -------------------------------------------------------------------------------- /blog-api/config/.gitignore: -------------------------------------------------------------------------------- 1 | .merge-plan.php 2 | -------------------------------------------------------------------------------- /blog-api/config/common/bootstrap.php: -------------------------------------------------------------------------------- 1 | Cache::class, 11 | CacheInterface::class => FileCache::class, 12 | ]; 13 | -------------------------------------------------------------------------------- /blog-api/config/common/di/cycle.php: -------------------------------------------------------------------------------- 1 | static function (DatabaseManager $dbManager, Spiral\Core\FactoryInterface $factory) { 16 | return new Factory( 17 | $dbManager, 18 | null, 19 | $factory, 20 | new DoctrineCollectionFactory() 21 | ); 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /blog-api/config/common/di/hydrator.php: -------------------------------------------------------------------------------- 1 | ContainerAttributeResolverFactory::class, 12 | ObjectFactoryInterface::class => ContainerObjectFactory::class, 13 | ]; 14 | -------------------------------------------------------------------------------- /blog-api/config/common/di/logger.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'class' => Logger::class, 15 | '__construct()' => [ 16 | 'targets' => ReferencesArray::from([ 17 | FileTarget::class, 18 | ]), 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /blog-api/config/common/di/psr17.php: -------------------------------------------------------------------------------- 1 | RequestFactory::class, 20 | ServerRequestFactoryInterface::class => ServerRequestFactory::class, 21 | ResponseFactoryInterface::class => ResponseFactory::class, 22 | StreamFactoryInterface::class => StreamFactory::class, 23 | UriFactoryInterface::class => UriFactory::class, 24 | UploadedFileFactoryInterface::class => UploadedFileFactory::class, 25 | ]; 26 | -------------------------------------------------------------------------------- /blog-api/config/common/di/router.php: -------------------------------------------------------------------------------- 1 | [ 23 | 'class' => UrlGenerator::class, 24 | 'setEncodeRaw()' => [$params['yiisoft/router-fastroute']['encodeRaw']], 25 | 'setDefaultArgument()' => ['_language', 'en'], 26 | 'reset' => function () { 27 | $this->defaultArguments = ['_language', 'en']; 28 | }, 29 | ], 30 | 31 | RouteCollectionInterface::class => static function (RouteCollectorInterface $collector) use ($config) { 32 | $collector 33 | ->middleware(FormatDataResponse::class) 34 | ->middleware(ExceptionMiddleware::class) 35 | ->middleware(RequestBodyParser::class) 36 | ->addGroup( 37 | Group::create('/{_language}')->routes(...$config->get('app-routes')), 38 | ) 39 | ->addGroup( 40 | Group::create()->routes(...$config->get('routes')), 41 | ); 42 | 43 | return new RouteCollection($collector); 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /blog-api/config/common/di/translator.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'definition' => static function (Aliases $aliases) use ($params) { 16 | return new CategorySource( 17 | $params['yiisoft/translator']['defaultCategory'], 18 | new MessageSource($aliases->get('@messages')), 19 | new IntlMessageFormatter(), 20 | ); 21 | }, 22 | 'tags' => ['translation.categorySource'], 23 | ], 24 | ]; 25 | -------------------------------------------------------------------------------- /blog-api/config/common/di/validator.php: -------------------------------------------------------------------------------- 1 | RuleHandlerContainer::class, 12 | ]; 13 | -------------------------------------------------------------------------------- /blog-api/config/console/commands.php: -------------------------------------------------------------------------------- 1 | Schema\SchemaCommand::class, 10 | 'cycle/schema/php' => Schema\SchemaPhpCommand::class, 11 | 'cycle/schema/clear' => Schema\SchemaClearCommand::class, 12 | 'cycle/schema/rebuild' => Schema\SchemaRebuildCommand::class, 13 | 'migrate/create' => Migration\CreateCommand::class, 14 | 'migrate/generate' => Migration\GenerateCommand::class, 15 | 'migrate/up' => Migration\UpCommand::class, 16 | 'migrate/down' => Migration\DownCommand::class, 17 | 'migrate/list' => Migration\ListCommand::class, 18 | ]; 19 | -------------------------------------------------------------------------------- /blog-api/config/console/params.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'commands' => require __DIR__ . '/commands.php', 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /blog-api/config/environments/dev/params.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'enabled' => false, 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /blog-api/config/environments/test/params.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'enabled' => false, 11 | ], 12 | 'yiisoft/yii-cycle' => [ 13 | 'dbal' => [ 14 | 'connections' => [ 15 | 'sqlite' => new SQLiteDriverConfig( 16 | new FileConnectionConfig(dirname(__DIR__, 3) . '/tests/Support/Data/database.db') 17 | ), 18 | ], 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /blog-api/config/web/di/application.php: -------------------------------------------------------------------------------- 1 | [ 15 | '__construct()' => [ 16 | 'dispatcher' => DynamicReference::to(static function (Injector $injector) use ($params) { 17 | return $injector->make(MiddlewareDispatcher::class) 18 | ->withMiddlewares($params['middlewares']); 19 | }), 20 | 'fallbackHandler' => Reference::to(NotFoundHandler::class), 21 | ], 22 | ], 23 | Yiisoft\Yii\Middleware\Locale::class => [ 24 | '__construct()' => [ 25 | 'supportedLocales' => $params['locale']['locales'], 26 | 'ignoredRequestUrlPatterns' => $params['locale']['ignoredRequests'], 27 | ], 28 | ], 29 | Yiisoft\Yii\Middleware\Subfolder::class => [ 30 | '__construct()' => [ 31 | 'prefix' => !empty(trim($_ENV['BASE_URL'] ?? '', '/')) ? $_ENV['BASE_URL'] : null, 32 | ], 33 | ], 34 | ]; 35 | -------------------------------------------------------------------------------- /blog-api/config/web/di/data-response.php: -------------------------------------------------------------------------------- 1 | ApiResponseFormatter::class, 18 | DataResponseFactoryInterface::class => DataResponseFactory::class, 19 | ContentNegotiator::class => [ 20 | '__construct()' => [ 21 | 'contentFormatters' => [ 22 | 'text/html' => new HtmlDataResponseFormatter(), 23 | 'application/xml' => new XmlDataResponseFormatter(), 24 | 'application/json' => new JsonDataResponseFormatter(), 25 | ], 26 | ], 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /blog-api/config/web/di/error-handler.php: -------------------------------------------------------------------------------- 1 | JsonRenderer::class, 14 | ]; 15 | -------------------------------------------------------------------------------- /blog-api/config/web/di/middleware-dispatcher.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'class' => CompositeParametersResolver::class, 18 | '__construct()' => [ 19 | Reference::to(HydratorAttributeParametersResolver::class), 20 | Reference::to(RequestInputParametersResolver::class), 21 | ], 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /blog-api/config/web/di/user.php: -------------------------------------------------------------------------------- 1 | UserRepository::class, 18 | IdentityWithTokenRepositoryInterface::class => UserRepository::class, 19 | AuthenticationMethodInterface::class => HttpHeader::class, 20 | Authentication::class => [ 21 | 'class' => Authentication::class, 22 | '__construct()' => [ 23 | 'authenticationFailureHandler' => Reference::to(AuthRequestErrorHandler::class), 24 | ], 25 | ], 26 | ]; 27 | -------------------------------------------------------------------------------- /blog-api/config/web/events.php: -------------------------------------------------------------------------------- 1 | [ 10 | static fn(TranslatorInterface $translator, SetLocaleEvent $event) => $translator->setLocale($event->getLocale()), 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /blog-api/config/web/params.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'requestInputParametersResolver' => [ 8 | 'throwInputValidationException' => true, 9 | ], 10 | ], 11 | ]; 12 | -------------------------------------------------------------------------------- /blog-api/configuration.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'params' => [ 8 | 'common/params.php', 9 | ], 10 | 'params-web' => [ 11 | '$params', 12 | 'web/params.php', 13 | ], 14 | 'params-console' => [ 15 | '$params', 16 | 'console/params.php', 17 | ], 18 | 'di' => 'common/di/*.php', 19 | 'di-web' => [ 20 | '$di', 21 | 'web/di/*.php', 22 | ], 23 | 'di-console' => '$di', 24 | 'di-providers' => [], 25 | 'di-providers-web' => [ 26 | '$di-providers', 27 | ], 28 | 'di-providers-console' => [ 29 | '$di-providers', 30 | ], 31 | 'di-delegates' => [], 32 | 'di-delegates-web' => [ 33 | '$di-delegates', 34 | ], 35 | 'di-delegates-console' => [ 36 | '$di-delegates', 37 | ], 38 | 'events' => [], 39 | 'events-web' => [ 40 | '$events', 41 | 'web/events.php', 42 | ], 43 | 'events-console' => '$events', 44 | 'app-routes' => 'common/routes.php', 45 | 'bootstrap' => 'common/bootstrap.php', 46 | 'bootstrap-web' => '$bootstrap', 47 | 'bootstrap-console' => '$bootstrap', 48 | ], 49 | 'config-plugin-environments' => [ 50 | 'dev' => [ 51 | 'params' => [ 52 | 'environments/dev/params.php', 53 | ], 54 | ], 55 | 'prod' => [ 56 | 'params' => [ 57 | 'environments/prod/params.php', 58 | ], 59 | ], 60 | 'test' => [ 61 | 'params' => [ 62 | 'environments/test/params.php', 63 | ], 64 | ], 65 | ], 66 | 'config-plugin-options' => [ 67 | 'source-directory' => 'config', 68 | ], 69 | ]; 70 | -------------------------------------------------------------------------------- /blog-api/data/db/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiisoft/demo/d2bfea8bfa7b4afd1ed7bcdd2f21e5fb35ba2cc2/blog-api/data/db/database.db -------------------------------------------------------------------------------- /blog-api/data/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | set $index_file "public/index.php"; 5 | set $root "/app"; 6 | 7 | root $root; 8 | index $index_file; 9 | 10 | error_log /var/log/nginx/error.log; #set 11 | access_log /var/log/nginx/access.log; #set 12 | 13 | location ~* \.(js|css|png|html)$ { 14 | root $root/public; 15 | access_log off; 16 | } 17 | 18 | location ~ [^/]\.php(/|$) { 19 | fastcgi_pass yii-php:9000; 20 | fastcgi_index $root/$index_file; 21 | include fastcgi_params; 22 | fastcgi_split_path_info ^(.+?\.php)(/.*)$; 23 | fastcgi_param PATH_INFO $fastcgi_path_info; 24 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 25 | } 26 | 27 | location / { 28 | try_files $uri $uri/ /$index_file?$query_string; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /blog-api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | php: 3 | container_name: yii-php 4 | image: yiisoftware/yii-php:8.3-fpm 5 | working_dir: /app 6 | volumes: 7 | - ./:/app 8 | nginx: 9 | image: nginx:alpine 10 | container_name: yii-nginx 11 | ports: 12 | - "8080:80" 13 | - "8081:81" 14 | volumes: 15 | - ./:/app 16 | - ./data/nginx/:/etc/nginx/conf.d/ 17 | depends_on: 18 | - php 19 | restart: always 20 | -------------------------------------------------------------------------------- /blog-api/docker/dev/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.1-alpine 2 | 3 | RUN apk add --no-cache curl 4 | 5 | WORKDIR /app/public 6 | 7 | COPY docker/dev/nginx/nginx.conf /etc/nginx/conf.d/default.conf 8 | COPY public ./ 9 | 10 | -------------------------------------------------------------------------------- /blog-api/docker/dev/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | server_name default; 4 | client_max_body_size 15m; 5 | root /app/public; 6 | resolver 127.0.0.11 ipv6=off; 7 | server_tokens off; 8 | 9 | location /health { 10 | add_header Content-Type text/plain; 11 | access_log off; 12 | return 200 'alive'; 13 | } 14 | 15 | location /assets { 16 | root /app/public; 17 | } 18 | 19 | location / { 20 | add_header 'Access-Control-Allow-Origin' '*' always; 21 | add_header 'Access-Control-Allow-Credentials' 'true' always; 22 | add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS' always; 23 | add_header 'Access-Control-Allow-Headers' 'Origin,Content-Type,Accept,Authorization' always; 24 | add_header 'Access-Control-Max-Age' 1728000; 25 | if ($request_method = 'OPTIONS') { 26 | add_header 'Content-Type' 'text/plain; charset=utf-8'; 27 | add_header 'Content-Length' 0; 28 | return 204; 29 | } 30 | try_files $uri /index.php?$args; 31 | } 32 | 33 | location ~ ^/index\.php(/|$) { 34 | set $upstream blog-api-backend:9000; 35 | fastcgi_read_timeout 60; 36 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 37 | fastcgi_pass $upstream; 38 | fastcgi_index index.php; 39 | include fastcgi_params; 40 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 41 | fastcgi_param SERVER_NAME $server_name; 42 | fastcgi_param PATH_INFO $fastcgi_path_info; 43 | } 44 | 45 | error_page 500 502 503 504 /50x.html; 46 | location = /50x.html { 47 | root /usr/share/nginx/html; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /blog-api/docker/dev/php/conf.d/php.ini: -------------------------------------------------------------------------------- 1 | ;apc.enable_cli = 1 2 | date.timezone = UTC 3 | session.auto_start = Off 4 | short_open_tag = Off 5 | expose_php = Off 6 | upload_max_filesize = 15M 7 | post_max_size = 150M 8 | memory_limit = 256M 9 | 10 | # https://symfony.com/doc/current/performance.html 11 | opcache.interned_strings_buffer = 16 12 | opcache.max_accelerated_files = 20000 13 | opcache.memory_consumption = 256 14 | realpath_cache_size = 4096K 15 | realpath_cache_ttl = 600 16 | max_execution_time = 300 17 | max_input_time = 300 18 | 19 | [xdebug] 20 | xdebug.mode = develop,debug 21 | xdebug.client_host = host.docker.internal 22 | ;xdebug.discover_client_host=true 23 | xdebug.show_error_trace = 0 24 | xdebug.start_with_request = trigger 25 | ;xdebug.client_host=host-gateway 26 | ;xdebug.start_with_request= 27 | xdebug.client_port = 9001 28 | xdebug.log_level = 10 29 | -------------------------------------------------------------------------------- /blog-api/docker/dev/php/php-fpm.d/www.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | daemonize = no 3 | process_control_timeout = 20 4 | error_log = /proc/self/fd/2 5 | 6 | [www] 7 | user = www-data 8 | group = www-data 9 | listen = 9000 10 | listen.mode = 0666 11 | ping.path = /ping 12 | ping.response = pong 13 | clear_env = off 14 | pm = dynamic 15 | pm.status_path = /status 16 | pm.max_children = 25 17 | pm.start_servers = 10 18 | pm.min_spare_servers = 5 19 | pm.max_spare_servers = 20 20 | pm.max_requests = 500 21 | -------------------------------------------------------------------------------- /blog-api/docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | ls -la 5 | # first arg is `-f` or `--some-option` 6 | if [ "${1#-}" != "$1" ]; then 7 | set -- php-fpm "$@" 8 | fi 9 | set -e 10 | 11 | if [ "$1" = 'php-fpm' ] || [ "$1" = 'php' ] || [ "$1" = 'yii' ]; then 12 | setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX runtime public 13 | setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX runtime public 14 | fi 15 | 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /blog-api/docker/prod/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.1-alpine 2 | 3 | RUN apk add --no-cache curl 4 | 5 | COPY docker/prod/nginx/nginx.conf /etc/nginx/conf.d/default.conf 6 | 7 | WORKDIR /app/public 8 | 9 | HEALTHCHECK --interval=30s --timeout=5s --start-period=1s CMD curl --fail http://127.0.0.1/health || exit 1 10 | -------------------------------------------------------------------------------- /blog-api/docker/prod/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | root /app/public; 4 | client_max_body_size 15m; 5 | resolver 127.0.0.11 ipv6=off; 6 | server_tokens off; 7 | 8 | location /health { 9 | add_header Content-Type text/plain; 10 | access_log off; 11 | return 200 'alive'; 12 | } 13 | 14 | location /assets { 15 | root /app/public; 16 | } 17 | 18 | location / { 19 | add_header 'Access-Control-Allow-Origin' '*' always; 20 | add_header 'Access-Control-Allow-Credentials' 'true' always; 21 | add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS' always; 22 | add_header 'Access-Control-Allow-Headers' 'Origin,Content-Type,Accept,Authorization' always; 23 | add_header 'Access-Control-Max-Age' 1728000; 24 | if ($request_method = 'OPTIONS') { 25 | add_header 'Content-Type' 'text/plain; charset=utf-8'; 26 | add_header 'Content-Length' 0; 27 | return 204; 28 | } 29 | try_files $uri /index.php?$args; 30 | } 31 | 32 | location ~ ^/index\.php(/|$) { 33 | set $upstream blog-api-backend:9000; 34 | fastcgi_read_timeout 60; 35 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 36 | fastcgi_pass $upstream; 37 | fastcgi_index index.php; 38 | include fastcgi_params; 39 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 40 | fastcgi_param SERVER_NAME $server_name; 41 | fastcgi_param PATH_INFO $fastcgi_path_info; 42 | } 43 | 44 | error_page 500 502 503 504 /50x.html; 45 | location = /50x.html { 46 | root /usr/share/nginx/html; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /blog-api/docker/prod/php/conf.d/php.ini: -------------------------------------------------------------------------------- 1 | ;apc.enable_cli = 1 2 | date.timezone = UTC 3 | session.auto_start = Off 4 | short_open_tag = Off 5 | expose_php = Off 6 | upload_max_filesize = 15M 7 | post_max_size = 150M 8 | memory_limit = 512M 9 | display_errors = Off 10 | 11 | # https://symfony.com/doc/current/performance.html 12 | opcache.interned_strings_buffer = 16 13 | opcache.max_accelerated_files = 20000 14 | opcache.memory_consumption = 256 15 | opcache.validate_timestamps = 0 16 | realpath_cache_size = 4096K 17 | realpath_cache_ttl = 600 18 | opcache.preload_user = www-data 19 | -------------------------------------------------------------------------------- /blog-api/docker/prod/php/php-fpm.d/www.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | daemonize = no 3 | process_control_timeout = 20 4 | error_log = /proc/self/fd/2 5 | 6 | [www] 7 | user = www-data 8 | group = www-data 9 | listen = 9000 10 | listen.mode = 0666 11 | clear_env = off 12 | ; access.suppress_path[] = /ping 13 | ping.path = /ping 14 | ping.response = pong 15 | pm = dynamic 16 | pm.status_path = /status 17 | pm.max_children = 25 18 | pm.start_servers = 10 19 | pm.min_spare_servers = 5 20 | pm.max_spare_servers = 20 21 | pm.max_requests = 500 22 | -------------------------------------------------------------------------------- /blog-api/infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "php:\/\/stderr", 9 | "stryker": { 10 | "report": "master" 11 | } 12 | }, 13 | "mutators": { 14 | "@default": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /blog-api/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests 21 | 22 | 23 | 24 | 25 | 26 | ./src 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /blog-api/psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /blog-api/public/.htaccess: -------------------------------------------------------------------------------- 1 | # use mod_rewrite for pretty URL support 2 | RewriteEngine on 3 | 4 | # if $showScriptName is false in UrlManager, do not allow accessing URLs with script name 5 | RewriteRule ^index.php/ - [L,R=404] 6 | 7 | # If a directory or a file exists, use the request directly 8 | RewriteCond %{REQUEST_FILENAME} !-f 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | 11 | # Otherwise forward the request to index.php 12 | RewriteRule . index.php 13 | -------------------------------------------------------------------------------- /blog-api/public/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /blog-api/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiisoft/demo/d2bfea8bfa7b4afd1ed7bcdd2f21e5fb35ba2cc2/blog-api/public/favicon.ico -------------------------------------------------------------------------------- /blog-api/public/index.php: -------------------------------------------------------------------------------- 1 | withTemporaryErrorHandler( 45 | new ErrorHandler( 46 | new Logger([new FileTarget(dirname(__DIR__) . '/runtime/logs/app.log')]), 47 | new JsonRenderer(), 48 | ) 49 | ); 50 | $runner->run(); 51 | -------------------------------------------------------------------------------- /blog-api/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /blog-api/resources/messages/de/app.php: -------------------------------------------------------------------------------- 1 | 'Seite nicht gefunden', 7 | ]; 8 | -------------------------------------------------------------------------------- /blog-api/resources/messages/en/app.php: -------------------------------------------------------------------------------- 1 | 'Page not found', 7 | ]; 8 | -------------------------------------------------------------------------------- /blog-api/resources/messages/ru/app.php: -------------------------------------------------------------------------------- 1 | 'Страница не найдена', 7 | ]; 8 | -------------------------------------------------------------------------------- /blog-api/runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /blog-api/src/Auth/AuthRequest.php: -------------------------------------------------------------------------------- 1 | login; 31 | } 32 | 33 | public function getPassword(): string 34 | { 35 | return $this->password; 36 | } 37 | 38 | public function getRules(): array 39 | { 40 | return [ 41 | 'login' => [ 42 | new Required(), 43 | ], 44 | 'password' => [ 45 | new Required(), 46 | ], 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /blog-api/src/Auth/AuthRequestErrorHandler.php: -------------------------------------------------------------------------------- 1 | postRepository = $postRepository; 19 | } 20 | 21 | /** 22 | * @psalm-return PaginatorInterface 23 | */ 24 | public function getPosts(int $page): PaginatorInterface 25 | { 26 | $dataReader = $this->postRepository->findAll(); 27 | 28 | /** @psalm-var PaginatorInterface */ 29 | return (new OffsetPaginator($dataReader)) 30 | ->withPageSize(self::POSTS_PER_PAGE) 31 | ->withCurrentPage($page); 32 | } 33 | 34 | /** 35 | * @param int $id 36 | * 37 | * @throws NotFoundException 38 | * 39 | * @return Post 40 | */ 41 | public function getPost(int $id): Post 42 | { 43 | /** 44 | * @var Post|null $post 45 | */ 46 | $post = $this->postRepository->findOne(['id' => $id]); 47 | if ($post === null) { 48 | throw new NotFoundException(); 49 | } 50 | 51 | return $post; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /blog-api/src/Blog/PostBuilder.php: -------------------------------------------------------------------------------- 1 | setTitle($request->getTitle()); 12 | $post->setContent($request->getText()); 13 | $post->setStatus($request->getStatus()); 14 | 15 | return $post; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /blog-api/src/Blog/PostFormatter.php: -------------------------------------------------------------------------------- 1 | $post->getId(), 23 | 'title' => $post->getTitle(), 24 | 'content' => $post->getContent(), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /blog-api/src/Blog/PostRepository.php: -------------------------------------------------------------------------------- 1 | orm = $orm; 19 | parent::__construct($select); 20 | } 21 | 22 | /** 23 | * @psalm-return EntityReader 24 | */ 25 | public function findAll(array $scope = [], array $orderBy = []): EntityReader 26 | { 27 | /** @psalm-var EntityReader */ 28 | return new EntityReader( 29 | $this 30 | ->select() 31 | ->where($scope) 32 | ->orderBy($orderBy) 33 | ); 34 | } 35 | 36 | public function save(Post $user): void 37 | { 38 | $transaction = new Transaction($this->orm); 39 | $transaction->persist($user); 40 | $transaction->run(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /blog-api/src/Blog/PostStatus.php: -------------------------------------------------------------------------------- 1 | getStatusCode() !== Status::OK) { 16 | return $this 17 | ->createErrorResponse() 18 | ->setErrorCode($response->getStatusCode()) 19 | ->setErrorMessage($this->getErrorMessage($response)); 20 | } 21 | 22 | return $this 23 | ->createSuccessResponse() 24 | ->setData($response->getData()); 25 | } 26 | 27 | public function createSuccessResponse(): ApiResponseData 28 | { 29 | return $this 30 | ->createResponse() 31 | ->setStatus('success'); 32 | } 33 | 34 | public function createErrorResponse(): ApiResponseData 35 | { 36 | return $this 37 | ->createResponse() 38 | ->setStatus('failed'); 39 | } 40 | 41 | public function createResponse(): ApiResponseData 42 | { 43 | return new ApiResponseData(); 44 | } 45 | 46 | private function getErrorMessage(DataResponse $response): string 47 | { 48 | $data = $response->getData(); 49 | if (is_string($data) && !empty($data)) { 50 | return $data; 51 | } 52 | 53 | return 'Unknown error'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /blog-api/src/Factory/RestGroupFactory.php: -------------------------------------------------------------------------------- 1 | Method::GET, 18 | 'list' => Method::GET, 19 | 'post' => Method::POST, 20 | 'put' => Method::PUT, 21 | 'delete' => Method::DELETE, 22 | 'patch' => Method::PATCH, 23 | 'options' => Method::OPTIONS, 24 | ]; 25 | 26 | public static function create(string $prefix, string $controller): Group 27 | { 28 | return Group::create($prefix)->routes(...self::createDefaultRoutes($controller)); 29 | } 30 | 31 | private static function createDefaultRoutes(string $controller): array 32 | { 33 | $routes = []; 34 | $reflection = new ReflectionClass($controller); 35 | foreach (self::METHODS as $methodName => $httpMethod) { 36 | if ($reflection->hasMethod($methodName)) { 37 | $pattern = ($methodName === 'list' || $methodName === 'post') ? '' : self::ENTITY_PATTERN; 38 | $routes[] = Route::methods([$httpMethod], $pattern)->action([$controller, $methodName]); 39 | } 40 | } 41 | if ($reflection->hasMethod('options')) { 42 | $routes[] = Route::methods([Method::OPTIONS], '')->action([$controller, 'options']); 43 | } 44 | 45 | return $routes; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /blog-api/src/Formatter/ApiResponseFormatter.php: -------------------------------------------------------------------------------- 1 | apiResponseDataFactory = $apiResponseDataFactory; 23 | $this->jsonDataResponseFormatter = $jsonDataResponseFormatter; 24 | } 25 | 26 | public function format(DataResponse $dataResponse): ResponseInterface 27 | { 28 | $response = $dataResponse->withData( 29 | $this->apiResponseDataFactory 30 | ->createFromResponse($dataResponse) 31 | ->toArray() 32 | ); 33 | 34 | return $this->jsonDataResponseFormatter->format($response); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /blog-api/src/Formatter/PaginatorFormatter.php: -------------------------------------------------------------------------------- 1 | $paginator->getPageSize(), 24 | 'currentPage' => $paginator->getCurrentPage(), 25 | 'totalPages' => $paginator->getTotalPages(), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /blog-api/src/Handler/NotFoundHandler.php: -------------------------------------------------------------------------------- 1 | formatter->format( 27 | $this->dataResponseFactory->createResponse( 28 | $this->translator->translate('404.title'), 29 | Status::NOT_FOUND, 30 | ) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /blog-api/src/InfoController.php: -------------------------------------------------------------------------------- 1 | createResponse(['version' => $this->versionProvider->version, 'author' => 'yiisoft']); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /blog-api/src/Installer.php: -------------------------------------------------------------------------------- 1 | dataResponseFactory = $dataResponseFactory; 23 | } 24 | 25 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 26 | { 27 | try { 28 | return $handler->handle($request); 29 | } catch (ApplicationException $e) { 30 | return $this->dataResponseFactory->createResponse($e->getMessage(), $e->getCode()); 31 | } catch (InputValidationException $e) { 32 | return $this->dataResponseFactory->createResponse($e->getResult()->getErrorMessages()[0], Status::BAD_REQUEST); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /blog-api/src/Queue/LoggingAuthorizationHandler.php: -------------------------------------------------------------------------------- 1 | logger->info('User is login', [ 21 | 'data' => $message->getData(), 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /blog-api/src/Queue/UserLoggedInMessage.php: -------------------------------------------------------------------------------- 1 | id = $id; 20 | } 21 | 22 | public function getId(): ?string 23 | { 24 | return $this->id; 25 | } 26 | 27 | public function getHandlerName(): string 28 | { 29 | return LoggingAuthorizationHandler::NAME; 30 | } 31 | 32 | public function getData(): array 33 | { 34 | return [ 35 | 'user_id' => $this->userId, 36 | 'time' => $this->time, 37 | ]; 38 | } 39 | 40 | public function getMetadata(): array 41 | { 42 | return []; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /blog-api/src/RestControllerTrait.php: -------------------------------------------------------------------------------- 1 | $user->getLogin(), 22 | 'created_at' => $user 23 | ->getCreatedAt() 24 | ->format('d.m.Y H:i:s'), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /blog-api/src/User/UserRepository.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | public function findAllOrderByLogin(): EntityReader 30 | { 31 | /** @psalm-var EntityReader */ 32 | return (new EntityReader($this->select())) 33 | ->withSort( 34 | Sort::only(['login'])->withOrderString('login') 35 | ); 36 | } 37 | 38 | public function findIdentity(string $id): ?IdentityInterface 39 | { 40 | return $this->findIdentityBy('id', $id); 41 | } 42 | 43 | public function findIdentityByToken(string $token, string $type = null): ?IdentityInterface 44 | { 45 | return $this->findIdentityBy('token', $token); 46 | } 47 | 48 | public function findByLogin(string $login): ?IdentityInterface 49 | { 50 | return $this->findIdentityBy('login', $login); 51 | } 52 | 53 | public function save(IdentityInterface $user): void 54 | { 55 | $this->entityWriter->write([$user]); 56 | } 57 | 58 | private function findIdentityBy(string $field, string $value): ?IdentityInterface 59 | { 60 | /** 61 | * @var $identity IdentityInterface|null 62 | */ 63 | return $this->findOne([$field => $value]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /blog-api/src/User/UserRequest.php: -------------------------------------------------------------------------------- 1 | user; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /blog-api/src/VersionProvider.php: -------------------------------------------------------------------------------- 1 | sendGET('/'); 15 | $I->seeResponseCodeIs(HttpCode::OK); 16 | $I->seeResponseIsJson(); 17 | $I->seeResponseContainsJson( 18 | [ 19 | 'status' => 'success', 20 | 'error_message' => '', 21 | 'error_code' => null, 22 | 'data' => [ 23 | 'version' => '3.0', 24 | 'author' => 'yiisoft', 25 | ], 26 | ] 27 | ); 28 | } 29 | 30 | public function testNotFoundPage(AcceptanceTester $I): void 31 | { 32 | $I->sendGET('/not_found_page'); 33 | $I->seeResponseCodeIs(HttpCode::NOT_FOUND); 34 | $I->seeResponseIsJson(); 35 | $I->seeResponseContainsJson( 36 | [ 37 | 'status' => 'failed', 38 | 'error_message' => 'Page not found', 39 | 'error_code' => 404, 40 | 'data' => null, 41 | ] 42 | ); 43 | } 44 | 45 | public function testNotFoundPageRu(AcceptanceTester $I): void 46 | { 47 | $I->sendGET('/ru/not_found_page'); 48 | $I->seeResponseCodeIs(HttpCode::NOT_FOUND); 49 | $I->seeResponseIsJson(); 50 | $I->seeResponseContainsJson( 51 | [ 52 | 'status' => 'failed', 53 | 'error_message' => 'Страница не найдена', 54 | 'error_code' => 404, 55 | 'data' => null, 56 | ] 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /blog-api/tests/Cli.suite.yml: -------------------------------------------------------------------------------- 1 | actor: CliTester 2 | modules: 3 | enabled: 4 | - Cli 5 | - \App\Tests\Support\Helper\Cli 6 | step_decorators: ~ 7 | -------------------------------------------------------------------------------- /blog-api/tests/Cli/ConsoleCest.php: -------------------------------------------------------------------------------- 1 | runShellCommand($command); 15 | $I->seeInShellOutput('Yii Console'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /blog-api/tests/Functional.suite.yml: -------------------------------------------------------------------------------- 1 | actor: FunctionalTester 2 | extensions: 3 | enabled: 4 | - Codeception\Extension\RunProcess: 5 | 0: php -d variables_order=EGPCS -S 127.0.0.1:8881 -t public 6 | sleep: 1 7 | modules: 8 | enabled: 9 | - PhpBrowser: 10 | url: http://127.0.0.1:8881%BASE_URL% 11 | - \App\Tests\Support\Helper\Functional 12 | step_decorators: ~ 13 | -------------------------------------------------------------------------------- /blog-api/tests/Functional/IndexControllerTest.php: -------------------------------------------------------------------------------- 1 | tester = new FunctionalTester(); 18 | } 19 | 20 | public function testGetIndex(): void 21 | { 22 | $method = 'GET'; 23 | $url = '/'; 24 | 25 | $this->tester->bootstrapApplication(dirname(__DIR__, 2)); 26 | $response = $this->tester->doRequest($method, $url); 27 | 28 | $this->assertEquals( 29 | [ 30 | 'status' => 'success', 31 | 'error_message' => '', 32 | 'error_code' => null, 33 | 'data' => ['version' => '3.0', 'author' => 'yiisoft'], 34 | ], 35 | $response->getContentAsJson() 36 | ); 37 | } 38 | 39 | public function testGetIndexMockVersion(): void 40 | { 41 | $method = 'GET'; 42 | $url = '/'; 43 | 44 | $this->tester->bootstrapApplication(dirname(__DIR__, 2)); 45 | 46 | $this->tester->mockService(VersionProvider::class, new VersionProvider('3.0.0')); 47 | 48 | $response = $this->tester->doRequest($method, $url); 49 | 50 | $this->assertEquals( 51 | [ 52 | 'status' => 'success', 53 | 'error_message' => '', 54 | 'error_code' => null, 55 | 'data' => ['version' => '3.0.0', 'author' => 'yiisoft'], 56 | ], 57 | $response->getContentAsJson() 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /blog-api/tests/Support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | run(); 18 | -------------------------------------------------------------------------------- /blog-api/yii.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | @setlocal 3 | set YII_PATH=%~dp0 4 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php 5 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 6 | @endlocal 7 | -------------------------------------------------------------------------------- /blog/.dockerignore: -------------------------------------------------------------------------------- 1 | runtime/ 2 | !runtime/.gitignore 3 | vendor/ 4 | -------------------------------------------------------------------------------- /blog/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | 13 | [*.php] 14 | ij_php_space_before_short_closure_left_parenthesis = false 15 | ij_php_space_after_type_cast = true 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | 20 | [*.yml] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /blog/.env.example: -------------------------------------------------------------------------------- 1 | YII_ENV= 2 | YII_DEBUG=true 3 | SENTRY_DSN= 4 | BASE_URL=/blog 5 | -------------------------------------------------------------------------------- /blog/.env.test: -------------------------------------------------------------------------------- 1 | YII_ENV=test 2 | YII_DEBUG=true 3 | SENTRY_DSN= 4 | BASE_URL=/ 5 | -------------------------------------------------------------------------------- /blog/.gitattributes: -------------------------------------------------------------------------------- 1 | # Autodetect text files 2 | * text=auto eol=lf 3 | 4 | # ...Unless the name matches the following overriding patterns 5 | 6 | # Definitively text files 7 | *.php text 8 | *.css text 9 | *.js text 10 | *.txt text 11 | *.md text 12 | *.xml text 13 | *.json text 14 | *.bat text 15 | *.sql text 16 | *.yml text 17 | 18 | # Ensure those won't be messed up with 19 | *.png binary 20 | *.jpg binary 21 | *.gif binary 22 | *.ttf binary 23 | 24 | # Avoid merge conflicts in CHANGELOG 25 | # https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ 26 | /CHANGELOG.md merge=union 27 | 28 | -------------------------------------------------------------------------------- /blog/.gitignore: -------------------------------------------------------------------------------- 1 | # IDE & OS files 2 | .*.swp 3 | .DS_Store 4 | .buildpath 5 | .idea 6 | .project 7 | .settings 8 | Thumbs.db 9 | nbproject 10 | 11 | # Binaries 12 | chkipper.phar 13 | composer.phar 14 | ocular.phar 15 | php-cs-fixer.phar 16 | phpstan.phar 17 | phpunit-skelgen.phar 18 | phpunit.phar 19 | 20 | # local config 21 | /.env 22 | /docker-compose.override.yml 23 | 24 | # evolving files 25 | /vendor 26 | /node_modules 27 | 28 | # Codeception 29 | c3.php 30 | -------------------------------------------------------------------------------- /blog/.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /blog/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /blog/Makefile: -------------------------------------------------------------------------------- 1 | init: composer-update npm-update up 2 | 3 | up: 4 | docker-compose up -d 5 | down: 6 | docker-compose down 7 | 8 | composer: 9 | docker-compose run php composer $(filter-out $@, $(MAKECMDGOALS)) 10 | composer-update: 11 | docker-compose run php composer update 12 | 13 | npm: 14 | docker-compose run php npm $(filter-out $@, $(MAKECMDGOALS)) 15 | npm-update: 16 | docker-compose run php npm update 17 | 18 | yii3: 19 | docker-compose run php ./yii $(filter-out $@, $(MAKECMDGOALS)) 20 | 21 | test: 22 | docker-compose run php ./vendor/bin/codecept run 23 | psalm: 24 | docker-compose run php ./vendor/bin/psalm 25 | -------------------------------------------------------------------------------- /blog/autoload.php: -------------------------------------------------------------------------------- 1 | load(); 11 | 12 | $_ENV['YII_ENV'] = empty($_ENV['YII_ENV']) ? null : (string) $_ENV['YII_ENV']; 13 | $_SERVER['YII_ENV'] = $_ENV['YII_ENV']; 14 | 15 | $_ENV['YII_DEBUG'] = filter_var( 16 | !empty($_ENV['YII_DEBUG']) ? $_ENV['YII_DEBUG'] : true, 17 | FILTER_VALIDATE_BOOLEAN, 18 | FILTER_NULL_ON_FAILURE 19 | ) ?? true; 20 | $_SERVER['YII_DEBUG'] = $_ENV['YII_DEBUG']; 21 | -------------------------------------------------------------------------------- /blog/codeception.yml: -------------------------------------------------------------------------------- 1 | namespace: App\Tests 2 | support_namespace: Support 3 | paths: 4 | tests: tests 5 | output: runtime/tests/_output 6 | data: tests/Support/Data 7 | support: tests/Support 8 | envs: tests/Support/Envs 9 | actor_suffix: Tester 10 | extensions: 11 | enabled: 12 | - Codeception\Extension\RunFailed 13 | settings: 14 | suite_class: \PHPUnit_Framework_TestSuite 15 | memory_limit: 1024M 16 | colors: true 17 | coverage: 18 | enabled: true 19 | whitelist: 20 | include: 21 | - src/* 22 | exclude: 23 | - src/Asset/* 24 | - src/Installer.php 25 | params: 26 | - .env 27 | - .env.test 28 | -------------------------------------------------------------------------------- /blog/config/.gitignore: -------------------------------------------------------------------------------- 1 | *-local.php 2 | .merge-plan.php 3 | -------------------------------------------------------------------------------- /blog/config/common/bootstrap.php: -------------------------------------------------------------------------------- 1 | FileCache::class, 14 | 15 | YiiCacheInterface::class => Cache::class, 16 | ]; 17 | -------------------------------------------------------------------------------- /blog/config/common/di/cycle.php: -------------------------------------------------------------------------------- 1 | static function (DatabaseManager $dbManager, Spiral\Core\FactoryInterface $factory) { 16 | return new Factory( 17 | $dbManager, 18 | null, 19 | $factory, 20 | new DoctrineCollectionFactory() 21 | ); 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /blog/config/common/di/hydrator.php: -------------------------------------------------------------------------------- 1 | ContainerAttributeResolverFactory::class, 12 | ObjectFactoryInterface::class => ContainerObjectFactory::class, 13 | ]; 14 | -------------------------------------------------------------------------------- /blog/config/common/di/logger.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'class' => Logger::class, 15 | '__construct()' => [ 16 | 'targets' => ReferencesArray::from([ 17 | FileTarget::class, 18 | ]), 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /blog/config/common/di/mailer.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'class' => FileMailer::class, 13 | '__construct()' => [ 14 | 'path' => DynamicReference::to( 15 | static fn(Aliases $aliases) => $aliases->get('@runtime/mail') 16 | ), 17 | ], 18 | ], 19 | ]; 20 | -------------------------------------------------------------------------------- /blog/config/common/di/psr17.php: -------------------------------------------------------------------------------- 1 | RequestFactory::class, 20 | ServerRequestFactoryInterface::class => ServerRequestFactory::class, 21 | ResponseFactoryInterface::class => ResponseFactory::class, 22 | StreamFactoryInterface::class => StreamFactory::class, 23 | UriFactoryInterface::class => UriFactory::class, 24 | UploadedFileFactoryInterface::class => UploadedFileFactory::class, 25 | ]; 26 | -------------------------------------------------------------------------------- /blog/config/common/di/rbac.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'class' => ItemsStorage::class, 17 | '__construct()' => [ 18 | 'directory' => $params['yiisoft/aliases']['aliases']['@root'] . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'rbac', 19 | ], 20 | ], 21 | AssignmentsStorageInterface::class => [ 22 | 'class' => AssignmentsStorage::class, 23 | '__construct()' => [ 24 | 'directory' => $params['yiisoft/aliases']['aliases']['@root'] . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'rbac', 25 | ], 26 | ], 27 | AccessCheckerInterface::class => Manager::class, 28 | ]; 29 | -------------------------------------------------------------------------------- /blog/config/common/di/router.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'class' => UrlGenerator::class, 23 | 'setEncodeRaw()' => [$params['yiisoft/router-fastroute']['encodeRaw']], 24 | 'setDefaultArgument()' => ['_language', 'en'], 25 | 'reset' => function () { 26 | $this->defaultArguments = ['_language', 'en']; 27 | }, 28 | ], 29 | 30 | RouteCollectionInterface::class => static function (RouteCollectorInterface $collector) use ($config) { 31 | $collector 32 | ->middleware( 33 | CsrfTokenMiddleware::class, 34 | FormatDataResponse::class, 35 | ) 36 | ->addRoute( 37 | Group::create('/{_language}')->routes(...$config->get('app-routes')), 38 | Group::create()->routes(...$config->get('routes')), 39 | ); 40 | 41 | return new RouteCollection($collector); 42 | }, 43 | ]; 44 | -------------------------------------------------------------------------------- /blog/config/common/di/sentry.php: -------------------------------------------------------------------------------- 1 | GuzzleClient::class, 13 | HttpAsyncClient::class => [ 14 | 'class' => GuzzleClientAdapter::class, 15 | '__construct()' => [ 16 | Reference::to(HttpClient::class), 17 | ], 18 | ], 19 | ]; 20 | -------------------------------------------------------------------------------- /blog/config/common/di/translator.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'definition' => static function (Aliases $aliases) use ($params) { 16 | return new CategorySource( 17 | $params['yiisoft/translator']['defaultCategory'], 18 | new MessageSource($aliases->get('@messages')), 19 | new IntlMessageFormatter(), 20 | ); 21 | }, 22 | 'tags' => ['translation.categorySource'], 23 | ], 24 | ]; 25 | -------------------------------------------------------------------------------- /blog/config/common/di/validator.php: -------------------------------------------------------------------------------- 1 | RuleHandlerContainer::class, 12 | ]; 13 | -------------------------------------------------------------------------------- /blog/config/common/rbac-rules.php: -------------------------------------------------------------------------------- 1 | routes( 12 | Route::get('/') 13 | ->action([SiteController::class, 'index']) 14 | ->name('index'), 15 | ) 16 | ->host('backend.{_host}') 17 | ->namePrefix('backend/'), 18 | 19 | Route::get('/backend') 20 | ->action([SiteController::class, 'index']) 21 | ->name('index'), 22 | ]; 23 | -------------------------------------------------------------------------------- /blog/config/console/commands.php: -------------------------------------------------------------------------------- 1 | Serve::class, 9 | 'user/create' => App\User\Console\CreateCommand::class, 10 | 'user/assignRole' => App\User\Console\AssignRoleCommand::class, 11 | 'fixture/add' => App\Command\Fixture\AddCommand::class, 12 | 'fixture/schema/clear' => App\Command\Fixture\SchemaClearCommand::class, 13 | 'router/list' => App\Command\Router\ListCommand::class, 14 | 'translator/translate' => App\Command\Translation\TranslateCommand::class, 15 | ]; 16 | -------------------------------------------------------------------------------- /blog/config/console/di/translator-extractor.php: -------------------------------------------------------------------------------- 1 | [ 15 | '__construct()' => [ 16 | [ 17 | DynamicReference::to([ 18 | 'class' => ExtractorCategorySource::class, 19 | '__construct()' => [ 20 | 'app', 21 | // Please set the following to use extractor. 22 | // MessageReader and MessageWriter should be set to the SAME MessageSource. 23 | DynamicReference::to(static function (Aliases $aliases) { 24 | return new MessageSource($aliases->get('@messages')); 25 | }), 26 | DynamicReference::to(static function (Aliases $aliases) { 27 | return new MessageSource($aliases->get('@messages')); 28 | }), 29 | ], 30 | ]), 31 | ], 32 | ], 33 | ], 34 | ]; 35 | -------------------------------------------------------------------------------- /blog/config/console/events.php: -------------------------------------------------------------------------------- 1 | [ 10 | static fn (Timer $timer) => $timer->start('overall'), 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /blog/config/console/params.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'name' => Application::NAME, 10 | 'version' => Application::VERSION, 11 | 'autoExit' => false, 12 | 'commands' => require __DIR__ . '/commands.php', 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /blog/config/environments/dev/params.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'enabled' => false, 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /blog/config/environments/test/params.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'enabled' => false, 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /blog/config/web/di/application.php: -------------------------------------------------------------------------------- 1 | [ 16 | '__construct()' => [ 17 | 'dispatcher' => DynamicReference::to([ 18 | 'class' => MiddlewareDispatcher::class, 19 | 'withMiddlewares()' => [$params['middlewares']], 20 | ]), 21 | 'fallbackHandler' => Reference::to(NotFoundHandler::class), 22 | ], 23 | ], 24 | Locale::class => [ 25 | '__construct()' => [ 26 | 'supportedLocales' => $params['locale']['locales'], 27 | 'ignoredRequestUrlPatterns' => $params['locale']['ignoredRequests'], 28 | ], 29 | ], 30 | Subfolder::class => [ 31 | '__construct()' => [ 32 | 'prefix' => !empty(trim($_ENV['BASE_URL'] ?? '', '/')) ? $_ENV['BASE_URL'] : null, 33 | ], 34 | ], 35 | ]; 36 | -------------------------------------------------------------------------------- /blog/config/web/di/auth.php: -------------------------------------------------------------------------------- 1 | static function (ContainerInterface $container): IdentityRepository { 24 | return $container 25 | ->get(ORMInterface::class) 26 | ->getRepository(Identity::class); 27 | }, 28 | 29 | CookieMiddleware::class => static fn (CookieLogin $cookieLogin, LoggerInterface $logger) => new CookieMiddleware( 30 | $logger, 31 | new CookieEncryptor($params['yiisoft/cookies']['secretKey']), 32 | new CookieSigner($params['yiisoft/cookies']['secretKey']), 33 | [$cookieLogin->getCookieName() => CookieMiddleware::SIGN], 34 | ), 35 | 36 | CurrentUser::class => [ 37 | 'withSession()' => [Reference::to(SessionInterface::class)], 38 | 'withAccessChecker()' => [Reference::to(AccessCheckerInterface::class)], 39 | 'reset' => function () { 40 | $this->clear(); 41 | }, 42 | ], 43 | ]; 44 | -------------------------------------------------------------------------------- /blog/config/web/di/comment-service.php: -------------------------------------------------------------------------------- 1 | static function (ContainerInterface $container) { 13 | /** 14 | * @var CommentRepository $repository 15 | */ 16 | $repository = $container 17 | ->get(ORMInterface::class) 18 | ->getRepository(Comment::class); 19 | 20 | return new CommentService($repository); 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /blog/config/web/di/contact-mailer.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'class' => ContactMailer::class, 12 | '__construct()' => [ 13 | 'sender' => $params['mailer']['senderEmail'], 14 | 'to' => $params['mailer']['adminEmail'], 15 | ], 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /blog/config/web/di/middleware-dispatcher.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'class' => CompositeParametersResolver::class, 18 | '__construct()' => [ 19 | Reference::to(HydratorAttributeParametersResolver::class), 20 | Reference::to(RequestInputParametersResolver::class), 21 | ], 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /blog/config/web/di/rate-limit.php: -------------------------------------------------------------------------------- 1 | function (Aliases $aliases) { 16 | $cache = new FileCache($aliases->get('@runtime/rate-limiter')); 17 | 18 | return new SimpleCacheStorage($cache); 19 | }, 20 | CounterInterface::class => [ 21 | 'class' => Counter::class, 22 | '__construct()' => [ 23 | 'limit' => 7, 24 | 'periodInSeconds' => 10, 25 | ], 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /blog/config/web/events.php: -------------------------------------------------------------------------------- 1 | [ 12 | static fn(Timer $timer) => $timer->start('overall'), 13 | ], 14 | SetLocaleEvent::class => [ 15 | static fn(TranslatorInterface $translator, SetLocaleEvent $event) => $translator->setLocale($event->getLocale()), 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /blog/config/web/params.php: -------------------------------------------------------------------------------- 1 | [ 17 | RequestCatcherMiddleware::class, 18 | ErrorCatcher::class, 19 | SentryMiddleware::class, 20 | SessionMiddleware::class, 21 | CookieMiddleware::class, 22 | CookieLoginMiddleware::class, 23 | Subfolder::class, 24 | Locale::class, 25 | Router::class, 26 | ], 27 | 28 | 'locale' => [ 29 | 'locales' => ['en' => 'en-US', 'ru' => 'ru-RU', 'id' => 'id-ID', 'sk' => 'sk-SK', 'de' => 'de-DE'], 30 | 'ignoredRequests' => [ 31 | '/gii**', 32 | '/debug**', 33 | '/inspect**', 34 | ], 35 | ], 36 | 37 | 'yiisoft/widget' => [ 38 | 'defaultTheme' => 'bootstrap5', 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /blog/config/web/widgets.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'urlParameterProvider()' => [ 13 | Reference::to(UrlParameterProvider::class), 14 | ], 15 | 'urlCreator()' => [ 16 | Reference::to(UrlCreator::class), 17 | ], 18 | 'ignoreMissingPage()' => [true], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /blog/dependency-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "scan-files": [ 3 | "config/*.php", 4 | "src/*.php", 5 | "views/*.php", 6 | "public/index.php" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /blog/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | php: 3 | image: yiisoftware/yii-php:8.3-apache 4 | working_dir: /app 5 | volumes: 6 | - ./:/app 7 | # host-volume for composer cache 8 | - ~/.composer-docker/cache:/root/.composer/cache:delegated 9 | ports: 10 | - "30080:80" 11 | -------------------------------------------------------------------------------- /blog/docker/dev/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.1-alpine 2 | 3 | RUN apk add --no-cache curl 4 | 5 | WORKDIR /app/public 6 | 7 | COPY docker/dev/nginx/nginx.conf /etc/nginx/conf.d/default.conf 8 | COPY public ./ 9 | 10 | -------------------------------------------------------------------------------- /blog/docker/dev/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | server_name default; 4 | client_max_body_size 15m; 5 | root /app/public; 6 | resolver 127.0.0.11 ipv6=off; 7 | server_tokens off; 8 | 9 | location /health { 10 | add_header Content-Type text/plain; 11 | access_log off; 12 | return 200 'alive'; 13 | } 14 | 15 | location /assets { 16 | root /app/public; 17 | } 18 | 19 | location / { 20 | add_header 'Access-Control-Allow-Origin' '*' always; 21 | add_header 'Access-Control-Allow-Credentials' 'true' always; 22 | add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS' always; 23 | add_header 'Access-Control-Allow-Headers' 'Origin,Content-Type,Accept,Authorization' always; 24 | add_header 'Access-Control-Max-Age' 1728000; 25 | if ($request_method = 'OPTIONS') { 26 | add_header 'Content-Type' 'text/plain; charset=utf-8'; 27 | add_header 'Content-Length' 0; 28 | return 204; 29 | } 30 | try_files $uri /index.php?$args; 31 | } 32 | 33 | location ~ ^/index\.php(/|$) { 34 | set $upstream blog-backend:9000; 35 | fastcgi_read_timeout 60; 36 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 37 | fastcgi_pass $upstream; 38 | fastcgi_index index.php; 39 | include fastcgi_params; 40 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 41 | fastcgi_param SERVER_NAME $server_name; 42 | fastcgi_param PATH_INFO $fastcgi_path_info; 43 | } 44 | 45 | error_page 500 502 503 504 /50x.html; 46 | location = /50x.html { 47 | root /usr/share/nginx/html; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /blog/docker/dev/php/conf.d/php.ini: -------------------------------------------------------------------------------- 1 | ;apc.enable_cli = 1 2 | date.timezone = UTC 3 | session.auto_start = Off 4 | short_open_tag = Off 5 | expose_php = Off 6 | upload_max_filesize = 15M 7 | post_max_size = 150M 8 | memory_limit = 256M 9 | 10 | # https://symfony.com/doc/current/performance.html 11 | opcache.interned_strings_buffer = 16 12 | opcache.max_accelerated_files = 20000 13 | opcache.memory_consumption = 256 14 | realpath_cache_size = 4096K 15 | realpath_cache_ttl = 600 16 | max_execution_time = 300 17 | max_input_time = 300 18 | 19 | [xdebug] 20 | xdebug.mode = develop,debug 21 | xdebug.client_host = host.docker.internal 22 | ;xdebug.discover_client_host=true 23 | xdebug.show_error_trace = 0 24 | xdebug.start_with_request = trigger 25 | ;xdebug.client_host=host-gateway 26 | ;xdebug.start_with_request= 27 | xdebug.client_port = 9001 28 | xdebug.log_level = 10 29 | -------------------------------------------------------------------------------- /blog/docker/dev/php/php-fpm.d/www.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | daemonize = no 3 | process_control_timeout = 20 4 | error_log = /proc/self/fd/2 5 | 6 | [www] 7 | user = www-data 8 | group = www-data 9 | listen = 9000 10 | listen.mode = 0666 11 | ping.path = /ping 12 | ping.response = pong 13 | clear_env = off 14 | pm = dynamic 15 | pm.status_path = /status 16 | pm.max_children = 25 17 | pm.start_servers = 10 18 | pm.min_spare_servers = 5 19 | pm.max_spare_servers = 20 20 | pm.max_requests = 500 21 | -------------------------------------------------------------------------------- /blog/docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | ls -la 5 | # first arg is `-f` or `--some-option` 6 | if [ "${1#-}" != "$1" ]; then 7 | set -- php-fpm "$@" 8 | fi 9 | set -e 10 | 11 | if [ "$1" = 'php-fpm' ] || [ "$1" = 'php' ] || [ "$1" = 'yii' ]; then 12 | setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX runtime public 13 | setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX runtime public 14 | fi 15 | 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /blog/docker/prod/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.1-alpine 2 | 3 | RUN apk add --no-cache curl 4 | 5 | COPY docker/prod/nginx/nginx.conf /etc/nginx/conf.d/default.conf 6 | 7 | WORKDIR /app/public 8 | 9 | HEALTHCHECK --interval=30s --timeout=5s --start-period=1s CMD curl --fail http://127.0.0.1/health || exit 1 10 | -------------------------------------------------------------------------------- /blog/docker/prod/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | root /app/public; 4 | client_max_body_size 15m; 5 | resolver 127.0.0.11 ipv6=off; 6 | server_tokens off; 7 | 8 | location /health { 9 | add_header Content-Type text/plain; 10 | access_log off; 11 | return 200 'alive'; 12 | } 13 | 14 | location /assets { 15 | root /app/public; 16 | } 17 | 18 | location / { 19 | add_header 'Access-Control-Allow-Origin' '*' always; 20 | add_header 'Access-Control-Allow-Credentials' 'true' always; 21 | add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS' always; 22 | add_header 'Access-Control-Allow-Headers' 'Origin,Content-Type,Accept,Authorization' always; 23 | add_header 'Access-Control-Max-Age' 1728000; 24 | if ($request_method = 'OPTIONS') { 25 | add_header 'Content-Type' 'text/plain; charset=utf-8'; 26 | add_header 'Content-Length' 0; 27 | return 204; 28 | } 29 | try_files $uri /index.php?$args; 30 | } 31 | 32 | location ~ ^/index\.php(/|$) { 33 | set $upstream blog-backend:9000; 34 | fastcgi_read_timeout 60; 35 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 36 | fastcgi_pass $upstream; 37 | fastcgi_index index.php; 38 | include fastcgi_params; 39 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 40 | fastcgi_param SERVER_NAME $server_name; 41 | fastcgi_param PATH_INFO $fastcgi_path_info; 42 | } 43 | 44 | error_page 500 502 503 504 /50x.html; 45 | location = /50x.html { 46 | root /usr/share/nginx/html; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /blog/docker/prod/php/conf.d/php.ini: -------------------------------------------------------------------------------- 1 | ;apc.enable_cli = 1 2 | date.timezone = UTC 3 | session.auto_start = Off 4 | short_open_tag = Off 5 | expose_php = Off 6 | upload_max_filesize = 15M 7 | post_max_size = 150M 8 | memory_limit = 512M 9 | display_errors = Off 10 | 11 | # https://symfony.com/doc/current/performance.html 12 | opcache.interned_strings_buffer = 16 13 | opcache.max_accelerated_files = 20000 14 | opcache.memory_consumption = 256 15 | opcache.validate_timestamps = 0 16 | realpath_cache_size = 4096K 17 | realpath_cache_ttl = 600 18 | opcache.preload_user = www-data 19 | -------------------------------------------------------------------------------- /blog/docker/prod/php/php-fpm.d/www.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | daemonize = no 3 | process_control_timeout = 20 4 | error_log = /proc/self/fd/2 5 | 6 | [www] 7 | user = www-data 8 | group = www-data 9 | listen = 9000 10 | listen.mode = 0666 11 | clear_env = off 12 | ; access.suppress_path[] = /ping 13 | ping.path = /ping 14 | ping.response = pong 15 | pm = dynamic 16 | pm.status_path = /status 17 | pm.max_children = 25 18 | pm.start_servers = 10 19 | pm.min_spare_servers = 5 20 | pm.max_spare_servers = 20 21 | pm.max_requests = 500 22 | -------------------------------------------------------------------------------- /blog/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yii3-demo-blog", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "bootstrap": { 8 | "version": "5.3.3", 9 | "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", 10 | "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yii3-demo-blog", 3 | "version": "1.0.0", 4 | "description": "Yii3 Demo Blog", 5 | "dependencies": { 6 | "bootstrap": "^5.3.1" 7 | }, 8 | "devDependencies": {}, 9 | "license": "BSD-3-Clause" 10 | } 11 | -------------------------------------------------------------------------------- /blog/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests 21 | 22 | 23 | 24 | 25 | 26 | ./src 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /blog/psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /blog/public/.htaccess: -------------------------------------------------------------------------------- 1 | # use mod_rewrite for pretty URL support 2 | RewriteEngine on 3 | 4 | # if $showScriptName is false in UrlManager, do not allow accessing URLs with script name 5 | RewriteRule ^index.php/ - [L,R=404] 6 | 7 | # If a directory or a file exists, use the request directly 8 | RewriteCond %{REQUEST_FILENAME} !-f 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | 11 | # Otherwise forward the request to index.php 12 | RewriteRule . index.php 13 | -------------------------------------------------------------------------------- /blog/public/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /blog/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiisoft/demo/d2bfea8bfa7b4afd1ed7bcdd2f21e5fb35ba2cc2/blog/public/favicon.ico -------------------------------------------------------------------------------- /blog/public/index.php: -------------------------------------------------------------------------------- 1 | run(); 41 | -------------------------------------------------------------------------------- /blog/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /blog/resources/asset/css/site.css: -------------------------------------------------------------------------------- 1 | label.required:not(:empty):after { 2 | color: #dc3545; 3 | content: '\a0*'; 4 | } 5 | -------------------------------------------------------------------------------- /blog/resources/asset/js/app.js: -------------------------------------------------------------------------------- 1 | // app.js 2 | document.addEventListener('click', event => { 3 | if (event.target.matches('.load-more-comment')) { 4 | event.preventDefault(); 5 | event.target.disabled = true; 6 | 7 | const xhr = new XMLHttpRequest(); 8 | xhr.open('GET', event.target.href); 9 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 10 | xhr.send(); 11 | xhr.onreadystatechange = () => { 12 | if (xhr.readyState === 4) { 13 | if (xhr.status === 200) { 14 | event.target.parentNode.parentNode.remove(); 15 | document.querySelector(".comment-feed-container").insertAdjacentHTML('beforeend', xhr.responseText); 16 | } else { 17 | event.target.disabled = false; 18 | document.querySelector(".comment-feed-container").insertAdjacentText('beforeend', `An error occurred during your request: ${xhr.status}: ${xhr.statusText}`); 19 | } 20 | } 21 | }; 22 | } 23 | 24 | if (event.target.matches('#addTagButton')) { 25 | const input = document.getElementById('addTag'); 26 | if (input.value && !document.getElementById(`tag${input.value}`)) { 27 | const tags = document.getElementById('tags'); 28 | tags.insertAdjacentHTML('beforeend', 29 | ``); 33 | input.value = ''; 34 | } 35 | } 36 | 37 | if (event.target.matches('.remove-tag')) { 38 | event.target.remove(); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /blog/resources/backend/views/site/index.php: -------------------------------------------------------------------------------- 1 | setTitle('Backend'); 9 | ?> 10 | 11 | 12 |
13 |
14 |

Welcome to backend

15 |
16 |
17 | 18 |
19 |
20 | Just example table 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
#FirstLastHandle
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
50 |
51 |
52 | -------------------------------------------------------------------------------- /blog/resources/mail/layouts/html.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | --
20 | Mailed by Yii 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /blog/resources/rbac/items.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'name' => 'admin', 8 | 'type' => 'role', 9 | 'updatedAt' => 1599036348, 10 | 'createdAt' => 1599036348, 11 | 'children' => [ 12 | 'editPost', 13 | ], 14 | ], 15 | 'editPost' => [ 16 | 'name' => 'editPost', 17 | 'type' => 'permission', 18 | 'updatedAt' => 1599036348, 19 | 'createdAt' => 1599036348, 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /blog/resources/views/blog/_topTags.php: -------------------------------------------------------------------------------- 1 | 19 |

20 | Popular tags 21 |

22 |
    23 | 'list-group-item d-flex flex-column justify-content-between lh-condensed'] 27 | ); 28 | $blockEnd = Html::closeTag('li'); 29 | echo $blockBegin; 30 | if (count($tags)) { 31 | foreach ($tags->read() as $tagValue) { 32 | $label = $tagValue['label']; 33 | $count = (string) $tagValue['count']; 34 | 35 | echo Html::openTag('div', ['class' => 'd-flex justify-content-between align-items-center']); 36 | echo Html::a( 37 | Html::encode($label), 38 | $urlGenerator->generate('blog/tag', ['label' => $label]), 39 | ['class' => 'text-muted overflow-hidden'] 40 | ), ' ', Html::span($count, ['class' => 'badge rounded-pill bg-secondary']); 41 | echo Html::closeTag('div'); 42 | } 43 | } else { 44 | echo $translator->translate('tags not found'); 45 | } 46 | echo $blockEnd; 47 | ?> 48 |
49 | -------------------------------------------------------------------------------- /blog/resources/views/blog/archive/monthly-archive.php: -------------------------------------------------------------------------------- 1 | format('F'); 24 | $this->setTitle("Archive for $monthName $year"); 25 | 26 | $pagination = OffsetPagination::widget() 27 | ->paginator($paginator) 28 | ->urlGenerator( 29 | fn ($page) => $urlGenerator->generate( 30 | 'blog/archive/month', 31 | ['year' => $year, 'month' => $month, 'page' => $page] 32 | ) 33 | ); 34 | ?> 35 |

Archive for

36 |
37 |
38 | getCurrentPageSize(); 40 | if ($pageSize > 0) { 41 | echo Html::p( 42 | $translator->translate('layout.pagination-summary', ['pageSize' => $pageSize, 'total' => $paginator->getTotalItems()]), 43 | ['class' => 'text-muted'] 44 | ); 45 | } else { 46 | echo Html::p($translator->translate('layout.no-records')); 47 | } 48 | /** @var Post $item */ 49 | foreach ($paginator->read() as $item) { 50 | echo PostCard::widget()->post($item); 51 | } 52 | if ($pagination->isRequired()) { 53 | echo $pagination; 54 | } 55 | ?> 56 |
57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /blog/resources/views/blog/comments/_comments.php: -------------------------------------------------------------------------------- 1 | 18 | 19 | read() as $comment) { ?> 20 |
21 |
22 | #getId() ?> getCreatedAt() 24 | ->format('Y.m.d') ?> 25 |
26 |
27 |

getContent()) ?>

28 |
29 |
30 | 31 | 32 | isOnLastPage()) { ?> 33 | 41 | 42 | -------------------------------------------------------------------------------- /blog/resources/views/blog/comments/index.php: -------------------------------------------------------------------------------- 1 | setTitle($translator->translate('menu.comments-feed')); 20 | 21 | ?> 22 |

getTitle()) ?>

23 |
24 |
25 | render('_comments', ['data' => $data]) ?> 26 |
27 |
28 | -------------------------------------------------------------------------------- /blog/resources/views/blog/tag/index.php: -------------------------------------------------------------------------------- 1 | setTitle($item->getLabel()); 23 | 24 | $pagination = OffsetPagination::widget() 25 | ->paginator($paginator) 26 | ->urlGenerator(fn ($page) => $urlGenerator->generate( 27 | 'blog/tag', 28 | ['label' => $item->getLabel(), 'page' => $page] 29 | )); 30 | echo Html::tag('h1', Html::encode($item->getLabel())); 31 | echo Html::openTag('ul'); 32 | /** @var Post $post */ 33 | foreach ($paginator->read() as $post) { 34 | echo Html::openTag('li', ['class' => 'text-muted']); 35 | echo Html::a(Html::encode($post->getTitle()), $urlGenerator->generate('blog/post', ['slug' => $post->getSlug()])); 36 | echo ' by '; 37 | $userLogin = $post 38 | ->getUser() 39 | ->getLogin(); 40 | echo Html::a(Html::encode($userLogin), $urlGenerator->generate('user/profile', ['login' => $userLogin])); 41 | echo ' at '; 42 | echo Html::span($post 43 | ->getPublishedAt() 44 | ->format('H:i d.m.Y')); 45 | echo Html::closeTag('li'); 46 | } 47 | echo Html::closeTag('ul'); 48 | 49 | if ($pagination->isRequired()) { 50 | echo $pagination; 51 | } 52 | -------------------------------------------------------------------------------- /blog/resources/views/signup/signup.php: -------------------------------------------------------------------------------- 1 | setTitle($translator->translate('signup')); 21 | ?> 22 | 23 |
24 |
25 |
26 |
27 |
28 |

getTitle()) ?>

29 |
30 |
31 | post($urlGenerator->generate('auth/signup')) 33 | ->csrf($csrf) 34 | ->id('signupForm') 35 | ->open() ?> 36 | 37 | autofocus() ?> 38 | 39 | 40 | buttonId('register-button') 42 | ->name('register-button') 43 | ->content($translator->translate('layout.submit')) 44 | ?> 45 | 46 | close() ?> 47 |
48 |
49 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /blog/resources/views/site/404.php: -------------------------------------------------------------------------------- 1 | setTitle($translator->translate('layout.not-found')); 18 | ?> 19 | 20 |
21 |
22 |

404

23 |

24 | translate('layout.page.not-found', [ 25 | 'url' => Html::span( 26 | Html::encode($currentRoute 27 | ->getUri() 28 | ->getPath()), 29 | ['class' => 'text-muted'] 30 | ), 31 | ]) 32 | ?> 33 |

34 |

35 | translate('layout.go.home'), 37 | $urlGenerator->generate('site/index'), 38 | ['class' => 'btn btn-outline-primary mt-5'] 39 | ); 40 | ?> 41 |

42 |
43 |
44 | -------------------------------------------------------------------------------- /blog/resources/views/user/profile.php: -------------------------------------------------------------------------------- 1 | setTitle('Profile'); 20 | 21 | $title = Html::encode($this->getTitle()); 22 | ?> 23 | 24 | attributes(['class' => 'container']) 26 | ->containerAttributes(['class' => 'row flex-column justify-content-center align-items-center']) 27 | ->data($item) 28 | ->dataAttributes(['class' => 'col-xl-5']) 29 | ->fields( 30 | DataField::create() 31 | ->attribute('id') 32 | ->label('ID') 33 | ->value($item->getId()), 34 | DataField::create() 35 | ->attribute('login') 36 | ->label($translator->translate('gridview.login')) 37 | ->value($item->getLogin()), 38 | DataField::create() 39 | ->attribute('create_at') 40 | ->label($translator->translate('gridview.create.at')) 41 | ->value($item->getCreatedAt()->format('H:i:s d.m.Y')), 42 | ) 43 | ->header(H2::tag()->class('text-center')->content("$title")->encode(false)->render()) 44 | ->labelAttributes(['class' => 'fw-bold']) 45 | ->valueAttributes(['class' => 'alert alert-info']); 46 | -------------------------------------------------------------------------------- /blog/runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /blog/src/Asset/AppAsset.php: -------------------------------------------------------------------------------- 1 | userRepository->findByLoginWithAuthIdentity($login); 24 | 25 | if ($user === null || !$user->validatePassword($password)) { 26 | return false; 27 | } 28 | 29 | return $this->currentUser->login($user->getIdentity()); 30 | } 31 | 32 | /** 33 | * @throws Throwable 34 | */ 35 | public function logout(): bool 36 | { 37 | $identity = $this->currentUser->getIdentity(); 38 | 39 | if ($identity instanceof Identity) { 40 | $identity->regenerateCookieLoginKey(); 41 | $this->identityRepository->save($identity); 42 | } 43 | 44 | return $this->currentUser->logout(); 45 | } 46 | 47 | public function getIdentity(): IdentityInterface 48 | { 49 | return $this->currentUser->getIdentity(); 50 | } 51 | 52 | public function isGuest(): bool 53 | { 54 | return $this->currentUser->isGuest(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /blog/src/Auth/Controller/SignupController.php: -------------------------------------------------------------------------------- 1 | viewRenderer = $viewRenderer->withControllerName('signup'); 20 | } 21 | 22 | public function signup( 23 | AuthService $authService, 24 | ServerRequestInterface $request, 25 | FormHydrator $formHydrator, 26 | SignupForm $signupForm 27 | ): ResponseInterface { 28 | if (!$authService->isGuest()) { 29 | return $this->redirectToMain(); 30 | } 31 | 32 | if ($formHydrator->populateFromPostAndValidate($signupForm, $request)) { 33 | $signupForm->signup(); 34 | return $this->redirectToMain(); 35 | } 36 | 37 | return $this->viewRenderer->render('signup', ['formModel' => $signupForm]); 38 | } 39 | 40 | private function redirectToMain(): ResponseInterface 41 | { 42 | return $this->webService->getRedirectResponse('site/index'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /blog/src/Auth/Identity.php: -------------------------------------------------------------------------------- 1 | regenerateCookieLoginKey(); 30 | } 31 | 32 | public function getId(): ?string 33 | { 34 | return $this->user->getId(); 35 | } 36 | 37 | public function getCookieLoginKey(): string 38 | { 39 | return $this->authKey; 40 | } 41 | 42 | public function getUser(): ?User 43 | { 44 | return $this->user; 45 | } 46 | 47 | public function validateCookieLoginKey(string $key): bool 48 | { 49 | return $this->authKey === $key; 50 | } 51 | 52 | public function regenerateCookieLoginKey(): void 53 | { 54 | $this->authKey = Random::string(32); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /blog/src/Auth/IdentityRepository.php: -------------------------------------------------------------------------------- 1 | findOne(['user_id' => $id]); 27 | } 28 | 29 | /** 30 | * @throws Throwable 31 | */ 32 | public function save(Identity $identity): void 33 | { 34 | $this->entityWriter->write([$identity]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /blog/src/Backend/Controller/SiteController.php: -------------------------------------------------------------------------------- 1 | viewRenderer = $viewRenderer 17 | ->withController($this) 18 | ->withViewPath('@resources/backend/views'); 19 | } 20 | 21 | public function index(): ResponseInterface 22 | { 23 | return $this->viewRenderer->render('index'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /blog/src/Blog/BlogController.php: -------------------------------------------------------------------------------- 1 | viewRenderer = $viewRenderer->withControllerName('blog'); 27 | } 28 | 29 | public function index( 30 | PostRepository $postRepository, 31 | TagRepository $tagRepository, 32 | ArchiveRepository $archiveRepo, 33 | CurrentUser $currentUser, 34 | #[RouteArgument('page')] $pageNum = 1, 35 | ): Response { 36 | $dataReader = $postRepository->findAllPreloaded(); 37 | $paginator = (new OffsetPaginator($dataReader)) 38 | ->withPageSize(self::POSTS_PER_PAGE) 39 | ->withCurrentPage((int)$pageNum); 40 | 41 | $data = [ 42 | 'paginator' => $paginator, 43 | 'archive' => $archiveRepo 44 | ->getFullArchive() 45 | ->withLimit(self::ARCHIVE_MONTHS_COUNT), 46 | 'tags' => $tagRepository->getTagMentions(self::POPULAR_TAGS_COUNT), 47 | 'isGuest' => $currentUser->isGuest(), 48 | ]; 49 | 50 | return $this->viewRenderer->render('index', $data); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /blog/src/Blog/Comment/CommentRepository.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function getReader(): DataReaderInterface 24 | { 25 | return (new EntityReader($this->select())) 26 | ->withSort($this->getSort()); 27 | } 28 | 29 | public function getSort(): Sort 30 | { 31 | return Sort::only(['id', 'public', 'created_at', 'post_id', 'user_id'])->withOrder(['id' => 'asc']); 32 | } 33 | 34 | public function findAll(array $scope = [], array $orderBy = []): DataReaderInterface 35 | { 36 | return new EntityReader($this 37 | ->select() 38 | ->where($scope) 39 | ->orderBy($orderBy)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /blog/src/Blog/Comment/CommentService.php: -------------------------------------------------------------------------------- 1 | repository->getReader())) 20 | ->withPageSize(self::COMMENTS_FEED_PER_PAGE); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /blog/src/Blog/Comment/Scope/PublicScope.php: -------------------------------------------------------------------------------- 1 | publicOrCondition = $publicOrCondition; 22 | } 23 | 24 | public function apply(QueryBuilder $query): void 25 | { 26 | // public or... 27 | if ($this->publicOrCondition !== null) { 28 | $query->where([ 29 | '@or' => [ 30 | ['public' => true], 31 | $this->publicOrCondition, 32 | ], 33 | ]); 34 | } else { 35 | $query->andWhere('public', '=', true); 36 | } 37 | // sort 38 | $query->orderBy('published_at', 'DESC'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /blog/src/Blog/Entity/PostTag.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | #[ManyToMany(target: Post::class, though: PostTag::class, fkAction: 'CASCADE', indexCreate: false)] 34 | private PivotedCollection $posts; 35 | 36 | public function __construct(string $label) 37 | { 38 | $this->label = $label; 39 | $this->created_at = new DateTimeImmutable(); 40 | $this->posts = new PivotedCollection(); 41 | } 42 | 43 | public function getId(): ?int 44 | { 45 | return $this->id; 46 | } 47 | 48 | public function getLabel(): string 49 | { 50 | return $this->label; 51 | } 52 | 53 | public function setLabel(string $label): void 54 | { 55 | $this->label = $label; 56 | } 57 | 58 | public function getCreatedAt(): DateTimeImmutable 59 | { 60 | return $this->created_at; 61 | } 62 | 63 | /** 64 | * @return Post[] 65 | */ 66 | public function getPosts(): array 67 | { 68 | return $this->posts->toArray(); 69 | } 70 | 71 | public function addPost(Post $post): void 72 | { 73 | $this->posts->add($post); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /blog/src/Blog/Post/PostForm.php: -------------------------------------------------------------------------------- 1 | title; 23 | } 24 | 25 | public function getContent(): ?string 26 | { 27 | return $this->content; 28 | } 29 | 30 | public function getFormName(): string 31 | { 32 | return ''; 33 | } 34 | 35 | public function getTags(): array 36 | { 37 | return $this->tags; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /blog/src/Blog/Post/PostService.php: -------------------------------------------------------------------------------- 1 | setTitle($form->getTitle()); 21 | $model->setContent($form->getContent()); 22 | $model->resetTags(); 23 | 24 | foreach ($form->getTags() as $tag) { 25 | $model->addTag($this->tagRepository->getOrCreate($tag)); 26 | } 27 | 28 | if ($model->isNewRecord()) { 29 | $model->setPublic(true); 30 | $model->setUser($user); 31 | } 32 | 33 | $this->repository->save($model); 34 | } 35 | 36 | public function getPostTags(Post $post): array 37 | { 38 | return array_map( 39 | static fn (Tag $tag) => $tag->getLabel(), 40 | $post->getTags() 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /blog/src/Blog/Post/Scope/PublicScope.php: -------------------------------------------------------------------------------- 1 | where(['public' => true]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /blog/src/Blog/Tag/TagController.php: -------------------------------------------------------------------------------- 1 | viewRenderer = $viewRenderer->withControllerName('blog/tag'); 22 | } 23 | 24 | public function index(CurrentRoute $currentRoute, TagRepository $tagRepository, PostRepository $postRepository, ResponseFactoryInterface $responseFactory): Response 25 | { 26 | $label = $currentRoute->getArgument('label'); 27 | $pageNum = (int) $currentRoute->getArgument('page', '1'); 28 | $item = $tagRepository->findByLabel($label); 29 | 30 | if ($item === null) { 31 | return $responseFactory->createResponse(404); 32 | } 33 | // preloading of posts 34 | $paginator = (new OffsetPaginator($postRepository->findByTag($item->getId()))) 35 | ->withPageSize(self::POSTS_PER_PAGE) 36 | ->withCurrentPage($pageNum); 37 | 38 | $data = [ 39 | 'item' => $item, 40 | 'paginator' => $paginator, 41 | ]; 42 | 43 | return $this->viewRenderer->render('index', $data); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /blog/src/Command/Translation/TranslateCommand.php: -------------------------------------------------------------------------------- 1 | addArgument('message', InputArgument::REQUIRED, 'Message that will be translated.'); 26 | $this->addArgument('locale', InputArgument::OPTIONAL, 'Translation locale.'); 27 | } 28 | 29 | protected function execute(InputInterface $input, OutputInterface $output) 30 | { 31 | $message = $input->getArgument('message'); 32 | $locale = $input->getArgument('locale'); 33 | 34 | $output->writeln($this->translator->translate($message, [], null, $locale)); 35 | 36 | return 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /blog/src/Contact/ContactController.php: -------------------------------------------------------------------------------- 1 | viewRenderer = $viewRenderer 25 | ->withControllerName('contact') 26 | ->withViewPath(__DIR__ . '/views'); 27 | } 28 | 29 | public function contact( 30 | FormHydrator $formHydrator, 31 | ServerRequestInterface $request, 32 | ): ResponseInterface { 33 | $form = new ContactForm(); 34 | if (!$formHydrator->populateFromPostAndValidate($form, $request)) { 35 | return $this->viewRenderer->render('form', ['form' => $form]); 36 | } 37 | 38 | $this->mailer->send($form); 39 | 40 | return $this->responseFactory 41 | ->createResponse(Status::FOUND) 42 | ->withHeader(Header::LOCATION, $this->url->generate('site/contact')); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /blog/src/Contact/ContactForm.php: -------------------------------------------------------------------------------- 1 | 'Name', 30 | 'email' => 'Email', 31 | 'subject' => 'Subject', 32 | 'body' => 'Body', 33 | ]; 34 | } 35 | 36 | public function getFormName(): string 37 | { 38 | return 'ContactForm'; 39 | } 40 | 41 | public function getRules(): array 42 | { 43 | return [ 44 | 'name' => [new Required()], 45 | 'email' => [new Required(), new Email()], 46 | 'subject' => [new Required()], 47 | 'body' => [new Required()], 48 | ]; 49 | } 50 | 51 | public function getPropertyTranslator(): ?PropertyTranslatorInterface 52 | { 53 | return new ArrayPropertyTranslator($this->getPropertyLabels()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /blog/src/Contact/mail/contact-email.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | Contact mail by Yii 13 | 14 | 15 | 16 |
17 | --
18 | Mailed by Yii 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /blog/src/Controller/Actions/ApiInfo.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 22 | } 23 | 24 | #[OA\Get( 25 | path: '/api/info/v2', 26 | description: '', 27 | summary: 'Returns info about the API', 28 | responses: [ 29 | new OA\Response( 30 | response:'200', 31 | description:'Success', 32 | content: new OA\JsonContent( 33 | allOf: [ 34 | new OA\Schema(ref: '#/components/schemas/Response'), 35 | new OA\Schema(properties: [ 36 | new OA\Property(property: 'data', properties: [ 37 | new OA\Property(property: 'version', type: 'string', example: '2.0'), 38 | new OA\Property(property: 'author', type: 'string', example: 'yiisoft'), 39 | ], type: 'object'), 40 | ]), 41 | ] 42 | ), 43 | ), 44 | ] 45 | )] 46 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 47 | { 48 | return $this->responseFactory->createResponse(['version' => '2.0', 'author' => 'yiisoft']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /blog/src/Controller/SiteController.php: -------------------------------------------------------------------------------- 1 | viewRenderer = $viewRenderer->withController($this); 15 | } 16 | 17 | public function index(): ResponseInterface 18 | { 19 | return $this->viewRenderer->render('index'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /blog/src/Handler/NotFoundHandler.php: -------------------------------------------------------------------------------- 1 | viewRenderer = $viewRenderer->withControllerName('site'); 20 | } 21 | 22 | public function handle(ServerRequestInterface $request): ResponseInterface 23 | { 24 | return $this->viewRenderer 25 | ->render('404') 26 | ->withStatus(Status::NOT_FOUND); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /blog/src/Installer.php: -------------------------------------------------------------------------------- 1 | permission === null) { 29 | throw new InvalidArgumentException('Permission not set.'); 30 | } 31 | 32 | if (!$this->userService->hasPermission($this->permission)) { 33 | return $this->responseFactory->createResponse(Status::FORBIDDEN); 34 | } 35 | 36 | return $handler->handle($request); 37 | } 38 | 39 | public function withPermission(string $permission): self 40 | { 41 | $new = clone $this; 42 | $new->permission = $permission; 43 | 44 | return $new; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /blog/src/Middleware/ApiDataWrapper.php: -------------------------------------------------------------------------------- 1 | handle($request); 27 | if ($response instanceof DataResponse) { 28 | $data = $response->getData(); 29 | if ($response->getStatusCode() !== 200) { 30 | if (is_string($data) && !empty($data)) { 31 | $message = $data; 32 | } else { 33 | $message = 'Unknown error'; 34 | } 35 | 36 | return $response->withData([ 37 | 'status' => 'failed', 38 | 'error' => ['message' => $message, 'status' => $response->getStatusCode()], 39 | ]); 40 | } 41 | 42 | return $response->withData(['status' => 'success', 'data' => $data]); 43 | } 44 | 45 | return $response; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /blog/src/Service/WebControllerService.php: -------------------------------------------------------------------------------- 1 | responseFactory 24 | ->createResponse(Status::FOUND) 25 | ->withHeader(Header::LOCATION, $this->urlGenerator->generate($url)); 26 | } 27 | 28 | public function getNotFoundResponse(): ResponseInterface 29 | { 30 | return $this->responseFactory 31 | ->createResponse(Status::NOT_FOUND); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /blog/src/Timer.php: -------------------------------------------------------------------------------- 1 | timers[$name] = microtime(true); 16 | } 17 | 18 | public function get(string $name): float 19 | { 20 | if (!array_key_exists($name, $this->timers)) { 21 | throw new InvalidArgumentException("There is no \"$name\" timer started"); 22 | } 23 | 24 | return microtime(true) - $this->timers[$name]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /blog/src/User/UserService.php: -------------------------------------------------------------------------------- 1 | currentUser->getId(); 22 | 23 | if ($userId === null) { 24 | return null; 25 | } 26 | 27 | return $this->repository->findById($this->currentUser->getId()); 28 | } 29 | 30 | public function hasPermission(string $permission): bool 31 | { 32 | $userId = $this->currentUser->getId(); 33 | 34 | return null !== $userId && $this->accessChecker->userHasPermission($userId, $permission); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /blog/src/ViewInjection/CommonViewInjection.php: -------------------------------------------------------------------------------- 1 | $this->url, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /blog/src/ViewInjection/LayoutViewInjection.php: -------------------------------------------------------------------------------- 1 | currentUser->getIdentity(); 20 | 21 | return [ 22 | 'brandLabel' => 'Yii Demo', 23 | 'user' => $identity instanceof Identity ? $identity->getUser() : null, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /blog/src/ViewInjection/LinkTagsViewInjection.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'rel' => 'icon', 16 | 'href' => '/favicon.ico', 17 | ], 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /blog/src/ViewInjection/MetaTagsViewInjection.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'name' => 'generator', 16 | 'content' => 'Yii', 17 | ], 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /blog/src/Widget/FlashMessage.php: -------------------------------------------------------------------------------- 1 | flash->getAll(); 20 | 21 | $html = []; 22 | foreach ($flashes as $type => $data) { 23 | foreach ($data as $message) { 24 | $html[] = Alert::widget()->addClass("alert-{$type} shadow")->body($message['body']); 25 | } 26 | } 27 | 28 | return implode($html); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /blog/src/Widget/PerformanceMetrics.php: -------------------------------------------------------------------------------- 1 | timer->get('overall'), 4); 19 | $memory = round(memory_get_peak_usage() / (1024 * 1024), 4); 20 | 21 | return "Time: $time s. Memory: $memory mb."; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /blog/tests/Acceptance.suite.yml: -------------------------------------------------------------------------------- 1 | actor: AcceptanceTester 2 | extensions: 3 | enabled: 4 | - Codeception\Extension\RunProcess: 5 | 0: php -d variables_order=EGPCS -S 127.0.0.1:8881 -t public 6 | sleep: 1 7 | modules: 8 | enabled: 9 | - PhpBrowser: 10 | url: http://127.0.0.1:8881%BASE_URL% 11 | - \App\Tests\Support\Helper\Acceptance 12 | step_decorators: ~ 13 | -------------------------------------------------------------------------------- /blog/tests/Acceptance/BlogPageCest.php: -------------------------------------------------------------------------------- 1 | wantTo('blog page works.'); 14 | $I->amOnPage('/blog'); 15 | } 16 | 17 | public function testBlogPage(AcceptanceTester $I): void 18 | { 19 | $I->expectTo('see blog page.'); 20 | $I->see('Blog'); 21 | $I->see('Popular tags'); 22 | $I->see('Archive'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /blog/tests/Acceptance/CommentPageCest.php: -------------------------------------------------------------------------------- 1 | wantTo('comment page works.'); 14 | $I->amOnPage('/blog/comments/'); 15 | } 16 | 17 | public function testCommentPage(AcceptanceTester $I): void 18 | { 19 | $I->expectTo('see comment page.'); 20 | $I->see('Comments Feed'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /blog/tests/Acceptance/ContactPageCest.php: -------------------------------------------------------------------------------- 1 | amOnPage('/contact'); 14 | } 15 | 16 | public function contactPageWorks(AcceptanceTester $I) 17 | { 18 | $I->wantTo('ensure that contact page works'); 19 | $I->seeElement('button', ['name' => 'contact-button']); 20 | } 21 | 22 | public function contactFormCanBeSubmitted(AcceptanceTester $I) 23 | { 24 | $I->amGoingTo('submit contact form with correct data'); 25 | $I->fillField('#contactform-name', 'tester'); 26 | $I->fillField('#contactform-email', 'tester@example.com'); 27 | $I->fillField('#contactform-subject', 'test subject'); 28 | $I->fillField('#contactform-body', 'test content'); 29 | 30 | $I->click('Submit'); 31 | 32 | $I->see("Thank you for contacting us, we'll get in touch with you as soon as possible."); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /blog/tests/Acceptance/IndexPageCest.php: -------------------------------------------------------------------------------- 1 | wantTo('index page works.'); 14 | $I->amOnPage('/'); 15 | } 16 | 17 | public function testIndexPage(AcceptanceTester $I): void 18 | { 19 | $I->expectTo('see page index.'); 20 | $I->see('Hello, everyone!'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /blog/tests/Acceptance/UserPageCest.php: -------------------------------------------------------------------------------- 1 | wantTo('user page works.'); 14 | $I->amOnPage('/user'); 15 | } 16 | 17 | public function testUserPage(AcceptanceTester $I): void 18 | { 19 | $I->expectTo('see user page.'); 20 | $I->seeLink('API v1 Info'); 21 | $I->seeLink('API v2 Info'); 22 | $I->seeLink('API Users List Data'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /blog/tests/Cli.suite.yml: -------------------------------------------------------------------------------- 1 | actor: CliTester 2 | modules: 3 | enabled: 4 | - Asserts 5 | - Cli 6 | step_decorators: ~ 7 | -------------------------------------------------------------------------------- /blog/tests/Cli/ConsoleCest.php: -------------------------------------------------------------------------------- 1 | runShellCommand($command); 16 | $I->seeInShellOutput('Yii Console'); 17 | } 18 | 19 | public function testCommandFixtureAdd(CliTester $I): void 20 | { 21 | $command = dirname(__DIR__, 2) . '/yii'; 22 | $I->runShellCommand($command . ' fixture/add'); 23 | $I->seeResultCodeIs(ExitCode::OK); 24 | } 25 | 26 | public function testCommandListCommand(CliTester $I): void 27 | { 28 | $command = dirname(__DIR__, 2) . '/yii'; 29 | $I->runShellCommand($command . ' list'); 30 | $I->seeResultCodeIs(ExitCode::OK); 31 | } 32 | 33 | public function testCommandUserCreateSuccessCommand(CliTester $I): void 34 | { 35 | $command = dirname(__DIR__, 2) . '/yii'; 36 | $I->runShellCommand($command . ' user/create user create123456'); 37 | $I->seeResultCodeIs(ExitCode::OK); 38 | } 39 | 40 | /** 41 | * Clear all data created with testCommandFixtureAdd(). 42 | * Clearing database prevents from getting errors during multiple continuous testing with other test, 43 | * what are based on empty database (eg, BlogPageCest). 44 | */ 45 | public function testCommandCycleSchemaClear(CliTester $I): void 46 | { 47 | $command = dirname(__DIR__, 2) . '/yii'; 48 | $I->runShellCommand($command . ' fixture/schema/clear'); 49 | $I->seeResultCodeIs(0); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /blog/tests/Functional.suite.yml: -------------------------------------------------------------------------------- 1 | actor: FunctionalTester 2 | extensions: 3 | enabled: 4 | - Codeception\Extension\RunProcess: 5 | 0: php -d variables_order=EGPCS -S 127.0.0.1:8881 -t public 6 | sleep: 1 7 | modules: 8 | enabled: 9 | - PhpBrowser: 10 | url: http://127.0.0.1:8881%BASE_URL% 11 | - \App\Tests\Support\Helper\Functional 12 | step_decorators: ~ 13 | -------------------------------------------------------------------------------- /blog/tests/Functional/ContactCest.php: -------------------------------------------------------------------------------- 1 | amOnPage('/contact'); 14 | } 15 | 16 | public function openContactPage(FunctionalTester $I) 17 | { 18 | $I->wantTo('ensure that contact page works'); 19 | $I->seeElement('button', ['name' => 'contact-button']); 20 | $I->see('Submit', ['name' => 'contact-button']); 21 | } 22 | 23 | public function submitEmptyForm(FunctionalTester $I) 24 | { 25 | $I->submitForm('#form-contact', []); 26 | $I->expectTo('see validations errors'); 27 | $I->see('Name cannot be blank.'); 28 | $I->see('Email cannot be blank.'); 29 | $I->see('Email is not a valid email address.'); 30 | $I->see('Subject cannot be blank.'); 31 | $I->see('Body cannot be blank.'); 32 | } 33 | 34 | public function submitFormWithIncorrectEmail(FunctionalTester $I) 35 | { 36 | $I->submitForm('#form-contact', [ 37 | 'ContactForm[name]' => 'tester', 38 | 'ContactForm[email]' => 'tester.email', 39 | 'ContactForm[subject]' => 'test subject', 40 | 'ContactForm[body]' => 'test content', 41 | 'ContactForm[verifyCode]' => 'testme', 42 | ]); 43 | $I->expectTo('see that email address is wrong'); 44 | $I->see('Email is not a valid email address.'); 45 | } 46 | 47 | public function submitFormSuccessfully(FunctionalTester $I) 48 | { 49 | $I->submitForm('#form-contact', [ 50 | 'ContactForm[name]' => 'tester', 51 | 'ContactForm[email]' => 'tester@example.com', 52 | 'ContactForm[subject]' => 'test subject', 53 | 'ContactForm[body]' => 'test content', 54 | ]); 55 | $I->see("Thank you for contacting us, we'll get in touch with you as soon as possible."); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /blog/tests/Functional/EventListenerConfigurationTest.php: -------------------------------------------------------------------------------- 1 | withDefinitions($config->get('di-console')) 33 | ->withProviders($config->get('di-providers-console')) 34 | ); 35 | 36 | $checker = $container->get(ListenerConfigurationChecker::class); 37 | 38 | $checker->check($config->get('events-console')); 39 | $checker->check($config->get('events-web')); 40 | 41 | self::assertInstanceOf(ListenerConfigurationChecker::class, $checker); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /blog/tests/Functional/IndexControllerTest.php: -------------------------------------------------------------------------------- 1 | tester = new FunctionalTester(); 17 | } 18 | 19 | public function testGetIndex() 20 | { 21 | $method = 'GET'; 22 | $url = '/'; 23 | 24 | $this->tester->bootstrapApplication(dirname(__DIR__, 2)); 25 | $response = $this->tester->doRequest($method, $url); 26 | 27 | $this->assertStringContainsString('Hello, everyone', $response->getContent()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /blog/tests/Support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | run(); 18 | -------------------------------------------------------------------------------- /blog/yii.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | @setlocal 3 | set YII_PATH=%~dp0 4 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php 5 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 6 | @endlocal 7 | -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.1-alpine 2 | 3 | RUN apk add --no-cache curl nano 4 | 5 | COPY ./html /usr/share/nginx/html 6 | 7 | 8 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | services: 4 | blog-backend: 5 | build: 6 | context: blog 7 | dockerfile: docker/dev/php/Dockerfile 8 | target: stage 9 | environment: 10 | YII_ENV: dev 11 | YII_DEBUG: "true" 12 | volumes: 13 | - ./blog:/app:delegated 14 | 15 | blog-nginx: 16 | build: 17 | context: blog 18 | dockerfile: docker/dev/nginx/Dockerfile 19 | 20 | blog-api-backend: 21 | build: 22 | context: blog-api 23 | dockerfile: docker/dev/php/Dockerfile 24 | target: stage 25 | environment: 26 | YII_ENV: dev 27 | YII_DEBUG: "true" 28 | volumes: 29 | - ./blog-api:/app:delegated 30 | 31 | blog-api-nginx: 32 | build: 33 | context: blog-api 34 | dockerfile: docker/dev/nginx/Dockerfile 35 | 36 | demo: 37 | restart: unless-stopped 38 | volumes: 39 | - ./demo/html:/usr/share/nginx 40 | 41 | gateway: 42 | build: 43 | context: gateway/nginx 44 | dockerfile: dev/Dockerfile 45 | volumes: 46 | - ./gateway/nginx/dev/templates:/etc/nginx/templates:ro 47 | -------------------------------------------------------------------------------- /gateway/nginx/dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.1-alpine AS nginx 2 | 3 | RUN apk add --no-cache curl nano 4 | 5 | WORKDIR /app 6 | 7 | RUN rm /etc/nginx/conf.d/default.conf 8 | COPY ./dev/templates/*.conf.template /etc/nginx/templates/ 9 | -------------------------------------------------------------------------------- /gateway/nginx/prod/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.1-alpine AS nginx 2 | 3 | RUN apk add --no-cache curl nano 4 | 5 | WORKDIR /app 6 | 7 | RUN rm /etc/nginx/conf.d/default.conf 8 | #COPY ./prod/templates/https.conf.template /etc/nginx/templates/ 9 | COPY ./prod/templates/*.conf.template /etc/nginx/templates/ 10 | 11 | #CMD ["nginx-debug", "-g", "daemon off;"] 12 | 13 | -------------------------------------------------------------------------------- /var/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !ssl/www/.gitignore 3 | !.gitignore -------------------------------------------------------------------------------- /var/ssl/www/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore --------------------------------------------------------------------------------