├── .dockerignore ├── .env-example ├── .gitignore ├── Dockerfile ├── README.md ├── assets └── AppAsset.php ├── build ├── Dockerfile ├── composer.json ├── docker-compose.yml └── etc │ ├── cont-init.d │ ├── 10-configure-php-fpm │ └── 15-configure-crond │ ├── nginx │ └── default.conf │ ├── php7 │ └── zz-docker.conf │ └── services.d │ ├── crond │ └── run │ ├── docker-stderr │ └── run │ ├── docker-stdout │ └── run │ ├── nginx │ └── run │ └── php7-fpm │ └── run ├── config ├── console.php ├── crontabs │ └── www-data ├── php │ └── productive.ini └── web.php ├── controllers ├── SiteController.php └── UserController.php ├── docker-compose-example.yml ├── helpers └── Mail.php ├── mail ├── layouts │ └── user.php └── user │ ├── confirm-email.php │ └── reset-password.php ├── migrations └── m150101_000000_init.php ├── models ├── User.php ├── behaviors │ └── TimestampBehavior.php ├── forms │ ├── LoginForm.php │ ├── PasswordResetRequestForm.php │ ├── ResetPasswordForm.php │ └── SignupForm.php └── queries │ └── UserQuery.php ├── runtime └── .gitignore ├── var └── sessions │ └── .gitignore ├── views ├── layouts │ └── main.php ├── site │ ├── error.php │ ├── index.php │ └── maintenance.php └── user │ ├── confirmed-email.php │ ├── email-confirmation-failed.php │ ├── login.php │ ├── password-was-reset.php │ ├── request-password-reset.php │ ├── requested-password-reset.php │ ├── reset-password.php │ ├── signed-up.php │ └── signup.php ├── web ├── assets │ └── .gitignore ├── css │ └── site.css └── index.php ├── yii └── yii.bat /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/* 2 | .env 3 | build/ 4 | config/local.php 5 | config/console-local.php 6 | runtime/ 7 | web/assets/ 8 | vendor/ 9 | var/ 10 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | # You probably want to set a project name for docker-compose 2 | # See this nasty bug, why this has to live inside this file 3 | # instead of docker-compose.yml: 4 | # https://github.com/docker/compose/issues/745 5 | COMPOSE_PROJECT_NAME="myapp" 6 | 7 | # Whether to enable debug mode in Yii. If not set this will be 0. 8 | YII_DEBUG=1 9 | 10 | # The application mode. If not set, this will be 'prod' 11 | YII_ENV=dev 12 | 13 | # The log trace level. If not set, this will be 0 14 | #YII_TRACELEVEL=0 15 | 16 | # Whether to show log messages from yii\* category for web requests and console commands. 17 | # If not set, this will be 0 18 | #WEB_LOG_YII=1 19 | #CONSOLE_LOG_YII=1 20 | 21 | # Whether to load config/(local|console-local).php. Default is 0. 22 | #ENABLE_LOCALCONF=1 23 | 24 | # Uncomment to enable maintenance mode 25 | #MAINTENANCE=1 26 | 27 | # Make sure that you provide a different unique cookie validation key in production 28 | COOKIE_VALIDATION_KEY="SeCrEt_DeV_Key--DO-NOT-USE-IN-PRODUCTION!" 29 | 30 | # DB credentials. If not set "web" is used as db, username and password. 31 | #DB_DSN=mysql:host=my.dbhost.com;dbname=web 32 | #DB_USER=user 33 | #DB_PASSWORD=secret 34 | 35 | # Configure a SMTP server here if you want to send emails 36 | #SMTP_HOST="" 37 | #SMTP_USER="" 38 | #SMTP_PASSWORD="" 39 | #SMTP_PORT=587 40 | #SMTP_ENCRYPTION=tls 41 | 42 | # Uncomment to disable caching 43 | #DISABLE_CACHE=1 44 | 45 | # Send all mails to this address instead of the actual recipients 46 | #MAIL_CATCHALL="you@example.com" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config/local.php 2 | config/console-local.php 3 | config/php/local.ini 4 | /docker-compose.yml 5 | /vendor/ 6 | 7 | .env 8 | *.bak 9 | *.swp 10 | .vimrc 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Application image 2 | # 3 | # This image mainly adds the latest application source to the base image 4 | # 5 | FROM myregistry.example.com/myproject/myapp:base-1.0 6 | 7 | # Copy PHP configuration into the image 8 | COPY ./config/php/productive.ini /etc/php7/conf.d/90-productive.ini 9 | 10 | # Copy the app code into the image 11 | COPY . /var/www/html 12 | 13 | # Create required directories listed in .dockerignore 14 | RUN mkdir -p runtime web/assets var/sessions \ 15 | && chown www-data:www-data runtime web/assets var/sessions 16 | 17 | # Let docker create a volume for the session dir. 18 | # This keeps the session files even if the container is rebuilt. 19 | VOLUME /var/www/html/var/sessions 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yii 2 Dockerized 2 | ================ 3 | 4 | A template for docker based Yii 2 applications. 5 | 6 | * Ephemeral container, configured via environment variables 7 | * Application specific base image (Nginx + PHP-FPM) 8 | * Optional local configuration overrides for development/debugging (git-ignored) 9 | * Base scaffold code for login, signup and forgot-password actions 10 | * Flat configuration file structure 11 | * Optional cron integration for periodic jobs 12 | 13 | > **Note:** The included example base image is now based on Alpine Linux and 14 | > uses [s6-overlay](https://github.com/just-containers/s6-overlay) to supervise 15 | > Nginx + PHP-FPM. You can of course change this to any setup you prefer. 16 | > You find the "old" setup based on Apache and mod_php in the "apache" branch. 17 | > Note though, that it's no longer maintained. 18 | 19 | # 1 Main Concepts 20 | 21 | ## 1.1 Base Image 22 | 23 | The core idea of this template is that you build a bespoke **base image** 24 | for your application that meets your project's exact requirements. This image 25 | contains: 26 | 27 | * PHP runtime environment (e.g. Nginx + PHP-FPM) 28 | * PHP extensions 29 | * Composer packages 30 | 31 | The base image will hardly ever change unless 32 | 33 | * you want to upgrade to a newer PHP or Yii version or 34 | * you want to add a missing PHP extension or composer package. 35 | 36 | Its configuration can be found in the `./build` directory: 37 | 38 | * `Dockerfile` adds PHP extensions and required system packages 39 | * `composer.json` and `composer.lock` list composer packages 40 | 41 | The actual **app image** extends from this base image and uses `./Dockerfile` 42 | in the main directory. It basically only adds your app sources and productive 43 | PHP config on top. 44 | 45 | In the recommended scenario you would build the base image once then upload 46 | it to your container registry and share it with your co-developers. 47 | 48 | If you don't have a container registry you can still use our template. But then 49 | each developer in your team will have to build the same base image locally. 50 | 51 | ## 1.2 Runtime Configuration via Environment Variables 52 | 53 | We follow docker's principle that containers are configured via environment 54 | variables. This only includes runtime configuration, of course: Things like 55 | whether to run in debug mode or DB credentials. Most other settings like for 56 | example URL rules will be hardcoded in `./config/web.php`. 57 | 58 | You should continue to follow this principle when developing your app. For 59 | more details also see our 60 | [yii2-configloader](https://github.com/codemix/yii2-configloader) that we use 61 | in this template. 62 | 63 | > **Note:** There's also one important main setting for php-fpm that affects 64 | > how many children should be started. This depends on the RAM you have 65 | > available. See `PHP_FPM_MAX_CHILDREN` in `docker-compose-example.yml`. 66 | 67 | # 2 Initial Project Setup 68 | 69 | ## 2.1 Download Application Template 70 | 71 | First fetch a copy of our application template, for example with git: 72 | 73 | ```sh 74 | git clone https://github.com/codemix/yii2-dockerized.git myproject 75 | rm -rf myproject/.git 76 | ``` 77 | 78 | You could also download the files as ZIP archive from GitHub. 79 | 80 | ## 2.2 Init Composer Packages 81 | 82 | To manage composer packages We use a container that is based on the official 83 | [composer](https://hub.docker.com/r/library/composer/) image. First go to the 84 | `./build` directory of the app: 85 | 86 | ```sh 87 | cd myproject/build 88 | ``` 89 | 90 | To add a package run: 91 | 92 | ```sh 93 | docker-compose run --rm composer require some/library 94 | ``` 95 | 96 | To update all packages run: 97 | 98 | ```sh 99 | docker-compose run --rm composer update 100 | ``` 101 | 102 | This will update `composer.json` and `composer.lock` respectively. You can 103 | also run other composer commands, of course. 104 | 105 | 106 | **You now have to rebuild your base image!** (see below). 107 | 108 | 109 | > **Note:** As docker's composer image may not meet the PHP requirements of all 110 | > your packages you may have to add `--ignore-platform-reqs` to be able to 111 | > install some packages. 112 | 113 | 114 | ## 2.3 Build the Base Image 115 | 116 | Before you continue with building the base image you should: 117 | 118 | * Set a tag name for the base image in `./build/docker-compose.yml` 119 | * Use the same tag name in `./Dockerfile` in the main directory 120 | * Optionally add more PHP extensions or system packages in `./build/Dockerfile` 121 | * Choose a timezone in `./build/Dockerfile`. This is only really relevant if 122 | you want to enable crond, to let the jobs run at correct local times. 123 | 124 | To start you first need to create an initial `composer.lock`. So go to the 125 | `./build` directory and run: 126 | 127 | ```sh 128 | docker-compose run --rm composer install 129 | ``` 130 | 131 | Then you can build the base image: 132 | 133 | ```sh 134 | docker-compose build 135 | ``` 136 | 137 | Now you could upload that image to your container registry. 138 | 139 | ## 2.4 Cleanup and Initial Commit 140 | 141 | At this point you may want to modify our application template, add some default 142 | configuration and remove those parts that you don't need. Afterwards you are 143 | ready for the initial commit to your project repository. 144 | 145 | 146 | # 3 Local Development 147 | 148 | During development we map the local app directory into the app image. 149 | This way we always run the code that we currently work on. 150 | 151 | As your local docker setup may differ from production (e.g. use different 152 | docker network settings) we usually keep `docker-compose.yml` out of version 153 | control. Instead we provide a `docker-compose-example.yml` with a reasonable 154 | example setup. 155 | 156 | Since the runtime configuration should happen with environment variables, we 157 | use a `.env` file. We also keep this out of version control and only include a 158 | `.env-example` to get developers started. 159 | 160 | ## 3.1 Initial Setup of a Development Environment 161 | 162 | After you've cloned your project you need to prepare two files: 163 | 164 | ```sh 165 | cd myproject 166 | cp docker-compose-example.yml docker-compose.yml 167 | cp .env-example .env 168 | ``` 169 | 170 | You should modify these two files e.g. to enable debug mode or configure a 171 | database. Then you should be ready to start your container and initialize 172 | your database (if your project has one): 173 | 174 | ```sh 175 | docker-compose up -d 176 | # Wait some seconds to let the DB container fire up ... 177 | docker-compose exec web ./yii migrate 178 | ``` 179 | 180 | Finally you need to set write permissions for some directories. It's sufficient 181 | if the `www-data` group in the container has write permissions. This way your 182 | local user can still own these directories: 183 | 184 | ```sh 185 | docker-compose exec web chgrp www-data web/assets runtime var/sessions 186 | docker-compose exec web chmod g+rwx web/assets runtime var/sessions 187 | ``` 188 | 189 | When done, you can access the new app from 190 | [http://localhost:8080](http://localhost:8080). 191 | 192 | 193 | ## 3.2 Development Session 194 | 195 | A development session will usually go like this: 196 | 197 | ```sh 198 | docker-compose up -d 199 | # edit files, check, tests, ... 200 | git add . 201 | git commit -m 'Implemented stuff' 202 | docker-compose stop 203 | ``` 204 | 205 | > **Note:** Another approach is to leave a terminal window open and start the 206 | > container with 207 | > 208 | > docker-compose up 209 | > 210 | > This way you can always follow the live logs of your container. To stop all 211 | > containers, press `Ctrl-c`. 212 | 213 | ### 3.2.1 Running yiic Commands 214 | 215 | If you need to run yiic commands you execute them in the running development 216 | container: 217 | 218 | ```sh 219 | docker-compose exec web ./yiic migrate/create add_column_name 220 | ``` 221 | 222 | > **Note:** If a command creates new files on your host (e.g. a new migration) 223 | > you may have to change file permissions to make them writeable on your host 224 | > system: 225 | > 226 | > chown -R mike migrations/* 227 | > 228 | 229 | ### 3.2.2 Adding or Updating Composer Packages 230 | 231 | The procedure for adding or updating composer packages is the same as described 232 | in 2.2 above. Remember that you have to rebuild the base image afterwards. It 233 | should probably also receive an updated version tag. 234 | 235 | ### 3.2.3 Working with Complex Local Configuration 236 | 237 | Sometimes you may have to add complex parts to `config/web.php` but want 238 | to avoid the risk of accidentally committing them. You therefore can activate 239 | support for a `config/local.php` file in your `.env`. This config file will 240 | be merged into `config/web.php`. 241 | 242 | ```sh 243 | # Whether to load config/(local|console-local).php. Default is 0. 244 | ENABLE_LOCALCONF=1 245 | ``` 246 | 247 | ### 3.2.3 Make Composer Packages Available Locally 248 | 249 | For some IDEs it's useful to have the composer packages available on your local 250 | host system. To do so you can simply copy them from inside the container: 251 | 252 | ``` 253 | docker-compose exec web cp -rf /var/www/vendor ./ 254 | ``` 255 | 256 | > **Note:** Inside the container composer packages live in `/var/www/vendor` 257 | > instead of the app's `./vendor` directory. This way we don't override the 258 | > vendor directory when we map the local app directory into the container. 259 | 260 | 261 | ### 3.2.3 Configuring Cron Jobs 262 | 263 | To run periodic jobs the integrated crond (busybox implementation) can be 264 | activated by setting `ENABLE_CROND=1` in the `docker-compose.yml` file. 265 | 266 | Cron jobs are added in `config/crontabs/`. There's an example file 267 | for `www-data` included. When changing a file there the container must be 268 | restarted to activate the crontab. 269 | -------------------------------------------------------------------------------- /assets/AppAsset.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 2.0 15 | */ 16 | class AppAsset extends AssetBundle 17 | { 18 | public $basePath = '@webroot'; 19 | public $baseUrl = '@web'; 20 | public $css = [ 21 | 'css/site.css', 22 | ]; 23 | public $js = [ 24 | ]; 25 | public $depends = [ 26 | 'yii\web\YiiAsset', 27 | 'yii\bootstrap\BootstrapAsset', 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | # Application base image 2 | # 3 | # This image contains: 4 | # 5 | # - PHP runtime 6 | # - PHP extensions 7 | # - Composer packages 8 | 9 | 10 | # Build stage 1: Install composer packages 11 | FROM composer AS vendor 12 | COPY composer.json /app 13 | COPY composer.lock /app 14 | RUN ["composer", "install", "--ignore-platform-reqs", "--prefer-dist"] 15 | 16 | 17 | # Build stage 2: Final image 18 | FROM alpine:3.11 19 | 20 | # Add the S6 supervisor overlay 21 | # https://github.com/just-containers/s6-overlay 22 | RUN wget -O /tmp/s6-overlay-amd64.tar.gz \ 23 | https://github.com/just-containers/s6-overlay/releases/download/v1.22.1.0/s6-overlay-amd64.tar.gz \ 24 | && tar xzf /tmp/s6-overlay-amd64.tar.gz -C / \ 25 | && rm /tmp/s6-overlay-amd64.tar.gz 26 | 27 | RUN apk add --no-cache \ 28 | # 29 | # Required packages 30 | nginx \ 31 | php7 \ 32 | php7-ctype \ 33 | php7-dom \ 34 | php7-fileinfo \ 35 | php7-fpm \ 36 | php7-intl \ 37 | php7-json \ 38 | php7-mbstring \ 39 | php7-posix \ 40 | php7-session \ 41 | php7-tokenizer \ 42 | # 43 | # Optional extensions (modify as needed) 44 | php7-apcu \ 45 | php7-opcache \ 46 | php7-pdo_mysql \ 47 | # 48 | # Ensure user/group www-data for php-fpm 49 | && adduser -u 82 -D -S -G www-data www-data \ 50 | # 51 | # Nginx: Create pid dir, send error logs to stderr and drop 52 | # access logs as they only duplicate the host Nginx logs 53 | && mkdir /run/nginx \ 54 | && ln -sf /dev/stderr /var/log/nginx/error.log \ 55 | && ln -sf /dev/null /var/log/nginx/access.log \ 56 | # 57 | # Create FIFOs for stderr/stdout of yii console and other processes. 58 | # Two s6 services then pipe the content to stdout/stderr. 59 | && mkfifo /docker-stdout && chmod 666 /docker-stdout \ 60 | && mkfifo /docker-stderr && chmod 666 /docker-stderr \ 61 | # 62 | # Set system timezone to make cron jobs run at correct local times 63 | && apk add --no-cache tzdata \ 64 | && cp /usr/share/zoneinfo/Europe/Berlin /etc/localtime \ 65 | && echo "Europe/Berlin" > /etc/timezone \ 66 | && apk del --force-broken-world tzdata 67 | 68 | # S6 configuration 69 | ADD ./etc/cont-init.d /etc/cont-init.d 70 | ADD ./etc/services.d /etc/services.d 71 | 72 | # Nginx default server and PHP defaults 73 | ADD ./etc/nginx/default.conf /etc/nginx/conf.d/default.conf 74 | ADD ./etc/php7/zz-docker.conf /etc/php7/php-fpm.d/zz-docker.conf 75 | 76 | # Composer packages from build stage 1 77 | COPY --from=vendor /var/www/vendor /var/www/vendor 78 | 79 | WORKDIR /var/www/html 80 | 81 | EXPOSE 80 82 | 83 | # S6 init will start all services 84 | ENTRYPOINT ["/init"] 85 | -------------------------------------------------------------------------------- /build/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codemix/yii2-dockerized", 3 | "description": "A template for docker based Yii 2 applications", 4 | "keywords": ["yii2", "framework", "docker", "application template"], 5 | "type": "project", 6 | "license": "MIT", 7 | "minimum-stability": "stable", 8 | "require": { 9 | "php": ">=5.4.0", 10 | "yiisoft/yii2": "2.0.40", 11 | "yiisoft/yii2-bootstrap": "^2.0.0", 12 | "yiisoft/yii2-swiftmailer": "^2.1.0", 13 | "codemix/yii2-configloader": "^0.10.0", 14 | "codemix/yii2-streamlog": "^1.3.0", 15 | "yiisoft/yii2-debug": "^2.1.0", 16 | "yiisoft/yii2-gii": "^2.1.0" 17 | }, 18 | "config": { 19 | "process-timeout": 1800, 20 | "vendor-dir": "/var/www/vendor" 21 | }, 22 | "repositories": [ 23 | { 24 | "type": "composer", 25 | "url": "https://asset-packagist.org" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | # Application base image 5 | # 6 | # This image contains: 7 | # 8 | # - PHP runtime 9 | # - PHP extensions 10 | # - Composer packages 11 | # 12 | base: 13 | # Specify a tag name for your base image here: 14 | image: myregistry.example.com/myproject/myapp:base-1.0 15 | build: ./ 16 | 17 | # Composer utility to manage composer packages 18 | # 19 | # You will usually only ever run one of these commands: 20 | # 21 | # docker-compose run --rm composer require [...] 22 | # docker-compose run --rm composer update [...] 23 | # 24 | # You may have to add --ignore-platform-reqs. 25 | # When changing packages you must rebuild the base container. 26 | # 27 | composer: 28 | image: composer 29 | volumes: 30 | - ./:/app 31 | entrypoint: composer 32 | -------------------------------------------------------------------------------- /build/etc/cont-init.d/10-configure-php-fpm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | 3 | if [ "$PHP_FPM_MAX_CHILDREN" ]; then 4 | if [ "$PHP_FPM_MAX_CHILDREN" -lt 5 ] 5 | then 6 | MIN_SPARE=1 7 | MAX_SPARE=1 8 | START=1 9 | else 10 | MIN_SPARE=$(( 20 * PHP_FPM_MAX_CHILDREN / 100 )) 11 | MAX_SPARE=$(( 60 * PHP_FPM_MAX_CHILDREN / 100 )) 12 | START=$(( MIN_SPARE + ( (MAX_SPARE - MIN_SPARE) / 2) )) 13 | fi 14 | echo "[10-configure-php-fpm]" \ 15 | "max_children:$PHP_FPM_MAX_CHILDREN" \ 16 | "start_servers:$START" \ 17 | "min_spare_servers:$MIN_SPARE" \ 18 | "max_spare_servers:$MAX_SPARE" 19 | sed -i "s/pm.max_children = .*/pm.max_children = $PHP_FPM_MAX_CHILDREN/" /etc/php7/php-fpm.d/www.conf 20 | sed -i "s/pm.start_servers = .*/pm.start_servers = $START/" /etc/php7/php-fpm.d/www.conf 21 | sed -i "s/pm.min_spare_servers = .*/pm.min_spare_servers = $MIN_SPARE/" /etc/php7/php-fpm.d/www.conf 22 | sed -i "s/pm.max_spare_servers = .*/pm.max_spare_servers = $MAX_SPARE/" /etc/php7/php-fpm.d/www.conf 23 | fi 24 | -------------------------------------------------------------------------------- /build/etc/cont-init.d/15-configure-crond: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | 3 | if [ "$ENABLE_CROND" ] && [ "$ENABLE_CROND" -eq "1" ] 4 | then 5 | echo "[15-configure-crond] Enabling crond and copying crontabs" 6 | rm -f /etc/services.d/crond/down 7 | rm -f /etc/crontabs/root 8 | cp /var/www/html/config/crontabs/* /etc/crontabs 9 | chown root:root /etc/crontabs/* 10 | chmod go-rwx /etc/crontabs/* 11 | else 12 | echo "[15-configure-crond] Disabling crond" 13 | touch /etc/services.d/crond/down 14 | fi 15 | -------------------------------------------------------------------------------- /build/etc/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | charset utf-8; 3 | client_max_body_size 0; 4 | listen 80; 5 | root /var/www/html/web; 6 | index index.php; 7 | 8 | location / { 9 | try_files $uri $uri/ /index.php$is_args$args; 10 | } 11 | 12 | location ~ \.(css|fla|gif|ico|js|jpe?g|mov|pdf|png|rar|swf|svg|woff|woff2|zip)$ { 13 | try_files $uri =404; 14 | } 15 | 16 | # deny accessing php files in the /assets directory 17 | location ~ ^/assets/.*\.php$ { 18 | deny all; 19 | } 20 | 21 | location ~ \.php$ { 22 | include fastcgi_params; 23 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 24 | fastcgi_buffering off; 25 | fastcgi_pass unix:/var/run/php7-fpm.sock; 26 | try_files $uri =404; 27 | } 28 | 29 | location ~* /\. { 30 | deny all; 31 | } 32 | } 33 | # vim:ft=nginx 34 | -------------------------------------------------------------------------------- /build/etc/php7/zz-docker.conf: -------------------------------------------------------------------------------- 1 | ; The settings below are mainly copied from the php-fpm docker image 2 | 3 | [global] 4 | error_log = /proc/self/fd/2 5 | 6 | ; Increased from 1024 to not wrap some longer lines (e.g. back traces) 7 | log_limit = 8192 8 | 9 | [www] 10 | 11 | clear_env = no 12 | 13 | ; Ensure worker stdout and stderr are sent to the main error log. 14 | catch_workers_output = yes 15 | 16 | ; Suppress warning when writing to stdout 17 | decorate_workers_output = no 18 | 19 | user = www-data 20 | group = www-data 21 | 22 | listen = /var/run/php7-fpm.sock 23 | listen.owner = www-data 24 | listen.group = www-data 25 | listen.mode = 0660 26 | -------------------------------------------------------------------------------- /build/etc/services.d/crond/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | 3 | export PATH="$PATH:/var/www/html" 4 | exec crond -L /dev/stdout -f 5 | -------------------------------------------------------------------------------- /build/etc/services.d/docker-stderr/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec tail -f /docker-stderr >&2 4 | -------------------------------------------------------------------------------- /build/etc/services.d/docker-stdout/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec tail -f /docker-stdout 4 | -------------------------------------------------------------------------------- /build/etc/services.d/nginx/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | 3 | exec nginx -g "daemon off;" 4 | -------------------------------------------------------------------------------- /build/etc/services.d/php7-fpm/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | 3 | exec php-fpm7 -F 4 | -------------------------------------------------------------------------------- /config/console.php: -------------------------------------------------------------------------------- 1 | web(); 5 | return [ 6 | 'id' => $web['id'], 7 | 'aliases' => $web['aliases'], 8 | 'basePath' => $web['basePath'], 9 | 'vendorPath' => $web['vendorPath'], 10 | 'bootstrap' => ['log'], 11 | 'controllerNamespace' => 'app\commands', 12 | 'components' => [ 13 | 'db' => $web['components']['db'], 14 | 'log' => [ 15 | 'traceLevel' => self::env('YII_TRACELEVEL', 0), 16 | 'flushInterval' => 1, // log messages immediately 17 | 'targets' => [ 18 | [ 19 | 'class' => 'codemix\streamlog\Target', 20 | 'url' => 'file:///docker-stdout', 21 | 'logVars' => [], 22 | 'levels' => ['info', 'trace'], 23 | 'except' => self::env('CONSOLE_LOG_YII', 0) ? [] : ['yii\*'], 24 | 'exportInterval' => 1, 25 | 'enableLocking' => true, 26 | 'disableTimestamp' => true, 27 | 'prefixString' => '[yii-console]', 28 | ], 29 | [ 30 | 'class' => 'codemix\streamlog\Target', 31 | 'url' => 'file:///docker-stderr', 32 | 'logVars' => [], 33 | 'levels' => ['error', 'warning'], 34 | 'except' => self::env('CONSOLE_LOG_YII', 0) ? [] : ['yii\*'], 35 | 'exportInterval' => 1, 36 | 'enableLocking' => true, 37 | 'disableTimestamp' => true, 38 | 'prefixString' => '[yii-console]', 39 | ], 40 | ], 41 | ], 42 | ], 43 | 'params' => $web['params'], 44 | ]; 45 | -------------------------------------------------------------------------------- /config/crontabs/www-data: -------------------------------------------------------------------------------- 1 | # 2 | # Crontab for user www-data 3 | # 4 | # Notes: 5 | # 6 | # - /var/www/html is in $PATH (see etc/services.d/crond/run) 7 | # - If a command started by the same crontab line is still running 8 | # it's not started again (extra feature of crond in busybox) 9 | # 10 | 11 | # min hour day month weekday command 12 | #* * * * * yii some/command 13 | -------------------------------------------------------------------------------- /config/php/productive.ini: -------------------------------------------------------------------------------- 1 | ; PHP ini settings for production mode 2 | 3 | ;upload_max_filesize = 64M 4 | ;post_max_size = 64M 5 | ;max_execution_time = 300 6 | ; 7 | ;session.use_strict_mode = 1 8 | ;session.gc_probability = 1 9 | ;session.gc_divisor = 100 10 | ;session.gc_maxlifetime = 1440 11 | -------------------------------------------------------------------------------- /config/web.php: -------------------------------------------------------------------------------- 1 | 'yii\debug\Module', 12 | 'allowedIPs' => ['*'], 13 | ]; 14 | $modules['gii'] = [ 15 | 'class' => 'yii\gii\Module', 16 | 'allowedIPs' => ['*'], 17 | ]; 18 | } 19 | 20 | return [ 21 | 'id' => 'basic', 22 | 'aliases' => [ 23 | '@bower' => '/var/www/vendor/bower-asset', 24 | '@npm' => '/var/www/vendor/npm-asset', 25 | ], 26 | 'basePath' => '/var/www/html', 27 | 'bootstrap' => $bootstrap, 28 | 'modules' => $modules, 29 | 'vendorPath' => '/var/www/vendor', 30 | 'catchAll' => self::env('MAINTENANCE', false) ? ['site/maintenance'] : null, 31 | 'components' => [ 32 | 'cache' => self::env('DISABLE_CACHE', false) ? 33 | 'yii\caching\DummyCache' : 34 | [ 35 | 'class' => 'yii\caching\ApcCache', 36 | 'useApcu' => true, 37 | ], 38 | 'db' => [ 39 | 'class' => 'yii\db\Connection', 40 | 'dsn' => self::env('DB_DSN', 'mysql:host=db;dbname=web'), 41 | 'username' => self::env('DB_USER', 'web'), 42 | 'password' => self::env('DB_PASSWORD', 'web'), 43 | 'charset' => 'utf8', 44 | 'tablePrefix' => '', 45 | ], 46 | 'errorHandler' => [ 47 | 'errorAction' => 'site/error', 48 | ], 49 | 'log' => [ 50 | 'traceLevel' => self::env('YII_TRACELEVEL', 0), 51 | 'flushInterval' => 1, 52 | 'targets' => [ 53 | [ 54 | 'class' => 'codemix\streamlog\Target', 55 | 'url' => 'php://stdout', 56 | 'logVars' => [], 57 | 'levels' => ['info', 'trace'], 58 | 'except' => self::env('WEB_LOG_YII', 0) ? [] : ['yii\*'], 59 | 'exportInterval' => 1, 60 | 'disableTimestamp' => true, 61 | 'prefixString' => '[yii-web]', 62 | ], 63 | [ 64 | 'class' => 'codemix\streamlog\Target', 65 | 'url' => 'php://stderr', 66 | 'logVars' => [], 67 | 'levels' => ['error', 'warning'], 68 | 'except' => self::env('WEB_LOG_YII', 0) ? [] : ['yii\*'], 69 | 'exportInterval' => 1, 70 | 'disableTimestamp' => true, 71 | 'prefixString' => '[yii-web]', 72 | ], 73 | ], 74 | ], 75 | 'mailer' => [ 76 | 'class' => 'yii\swiftmailer\Mailer', 77 | 'transport' => [ 78 | 'class' => 'Swift_SmtpTransport', 79 | 'host' => self::env('SMTP_HOST'), 80 | 'username' => self::env('SMTP_USER'), 81 | 'password' => self::env('SMTP_PASSWORD'), 82 | 'port' => self::env('SMTP_PORT', 25), 83 | 'encryption' => self::env('SMTP_ENCRYPTION', null), 84 | ], 85 | ], 86 | 'request' => [ 87 | 'cookieValidationKey' => self::env('COOKIE_VALIDATION_KEY', null, !YII_ENV_TEST), 88 | 'trustedHosts' => explode(',', self::env('PROXY_HOST', '192.168.0.0/24')), 89 | ], 90 | 'session' => [ 91 | 'name' => 'MYAPPSID', 92 | 'savePath' => '@app/var/sessions', 93 | 'timeout' => 1440, 94 | ], 95 | 'urlManager' => [ 96 | 'enablePrettyUrl' => true, 97 | 'showScriptName' => false, 98 | ], 99 | 'user' => [ 100 | 'identityClass' => 'app\models\User', 101 | 'enableAutoLogin' => true, 102 | 'loginUrl' => ['user/login'], 103 | ], 104 | ], 105 | 'params' => [ 106 | 'mail.from' => ['no-reply@example.com' => 'My Application'], 107 | 'mail.catchAll' => self::env('MAIL_CATCHALL', null), 108 | 109 | 'user.passwordResetTokenExpire' => 3600, 110 | 'user.emailConfirmationTokenExpire' => 43200, // 5 days 111 | ], 112 | ]; 113 | -------------------------------------------------------------------------------- /controllers/SiteController.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'class' => 'yii\web\ErrorAction', 18 | ], 19 | ]; 20 | } 21 | 22 | /** 23 | * Render the homepage 24 | */ 25 | public function actionIndex() 26 | { 27 | return $this->render('index'); 28 | } 29 | 30 | /** 31 | * @return string|\yii\web\Response the maintenance page or a redirect 32 | * response if not in maintenance mode 33 | */ 34 | public function actionMaintenance() 35 | { 36 | if (empty(Yii::$app->catchAll)) { 37 | return $this->redirect(Yii::$app->homeUrl); 38 | } 39 | Yii::$app->response->statusCode = 503; 40 | return $this->render('maintenance'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /controllers/UserController.php: -------------------------------------------------------------------------------- 1 | [ 26 | 'class' => AccessControl::className(), 27 | 'only' => ['logout', 'signup'], 28 | 'rules' => [ 29 | [ 30 | 'actions' => ['signup'], 31 | 'allow' => true, 32 | 'roles' => ['?'], 33 | ], 34 | [ 35 | 'actions' => ['logout'], 36 | 'allow' => true, 37 | 'roles' => ['@'], 38 | ], 39 | ], 40 | ], 41 | 'verbs' => [ 42 | 'class' => VerbFilter::className(), 43 | 'actions' => [ 44 | 'logout' => ['post'], 45 | ], 46 | ], 47 | ]; 48 | } 49 | 50 | /** 51 | * @return string|\yii\web\Response the login form or a redirect response 52 | */ 53 | public function actionLogin() 54 | { 55 | if (!Yii::$app->user->isGuest) { 56 | return $this->goHome(); 57 | } 58 | 59 | $model = new LoginForm(); 60 | if ($model->load(Yii::$app->request->post()) && $model->login()) { 61 | return $this->goBack(); 62 | } else { 63 | return $this->render('login', [ 64 | 'model' => $model, 65 | ]); 66 | } 67 | } 68 | 69 | /** 70 | * @return \yii\web\Response a redirect response 71 | */ 72 | public function actionLogout() 73 | { 74 | Yii::$app->user->logout(); 75 | 76 | return $this->goHome(); 77 | } 78 | 79 | /** 80 | * @return string|\yii\web\Response the signup form, the signup message or 81 | * a redirect response 82 | */ 83 | public function actionSignup() 84 | { 85 | if (Yii::$app->session->hasFlash('user-signed-up')) { 86 | return $this->render('signed-up'); 87 | } 88 | 89 | $model = new SignupForm; 90 | if ($model->load(Yii::$app->request->post()) && $model->signup() !== null) { 91 | Yii::$app->session->setFlash('user-signed-up'); 92 | return $this->refresh(); 93 | } else { 94 | return $this->render('signup', [ 95 | 'model' => $model, 96 | ]); 97 | } 98 | } 99 | 100 | /** 101 | * @return string|\yii\web\Response the confirmation failure message or a 102 | * redirect response 103 | */ 104 | public function actionConfirmEmail($token) 105 | { 106 | if (Yii::$app->session->hasFlash('user-confirmed-email')) { 107 | return $this->render('confirmed-email'); 108 | } 109 | 110 | $user = User::find() 111 | ->emailConfirmationToken($token) 112 | ->one(); 113 | 114 | if ($user !== null && $user->confirmEmail()) { 115 | Yii::$app->session->setFlash('user-confirmed-email'); 116 | return $this->refresh(); 117 | } else { 118 | return $this->render('email-confirmation-failed'); 119 | } 120 | } 121 | 122 | /** 123 | * @return string|\yii\web\Response the form to request a password reset or 124 | * a redirect response 125 | */ 126 | public function actionRequestPasswordReset() 127 | { 128 | if (Yii::$app->session->hasFlash('user-requested-password-reset')) { 129 | return $this->render('requested-password-reset'); 130 | } 131 | 132 | $model = new PasswordResetRequestForm(); 133 | if ($model->load(Yii::$app->request->post()) && $model->sendEmail()) { 134 | Yii::$app->session->setFlash('user-requested-password-reset'); 135 | return $this->refresh(); 136 | } else { 137 | return $this->render('request-password-reset', [ 138 | 'model' => $model, 139 | ]); 140 | } 141 | } 142 | 143 | /** 144 | * @return string|\yii\web\Response the form to reset the password or a 145 | * redirect response 146 | */ 147 | public function actionResetPassword($token) 148 | { 149 | if (Yii::$app->session->hasFlash('user-password-was-reset')) { 150 | return $this->render('password-was-reset'); 151 | } 152 | 153 | try { 154 | $model = new ResetPasswordForm($token); 155 | } catch (InvalidParamException $e) { 156 | throw new BadRequestHttpException($e->getMessage()); 157 | } 158 | 159 | if ($model->load(Yii::$app->request->post()) && $model->resetPassword()) { 160 | Yii::$app->session->setFlash('user-password-was-reset'); 161 | return $this->refresh(); 162 | } 163 | 164 | return $this->render('reset-password', [ 165 | 'model' => $model, 166 | ]); 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /docker-compose-example.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | web: 5 | build: ./ 6 | # During development we map local files into the container 7 | volumes: 8 | # Map current working copy into the container 9 | - ./:/var/www/html/ 10 | 11 | # Uncomment to use dev specific PHP settings 12 | #- ./config/php/local.ini:/etc/php7/conf.d/99-local.ini 13 | 14 | # Uncomment to use the local vendor directory, e.g. for debugging. 15 | # This requires that you have copied the directory to your host with 16 | # docker-compose run --rm web cp -ra /var/www/vendor . 17 | #- ./vendor:/var/www/vendor 18 | links: 19 | - db 20 | environment: 21 | # Make sure to tweak this in production 22 | PHP_FPM_MAX_CHILDREN: 5 23 | # Enable periodic jobs (see config/crontabs) 24 | #ENABLE_CROND: 1 25 | ports: 26 | - "8080:80" 27 | 28 | db: 29 | image: mysql:5 30 | environment: 31 | MYSQL_ROOT_PASSWORD: root 32 | MYSQL_DATABASE: web 33 | MYSQL_USER: web 34 | MYSQL_PASSWORD: web 35 | -------------------------------------------------------------------------------- /helpers/Mail.php: -------------------------------------------------------------------------------- 1 | email, 'user/' . $view, 'layouts/user', $params); 31 | } 32 | 33 | /** 34 | * Send an email 35 | * 36 | * The sender can be specified in `$params['from']` and defaults to the 37 | * `mail.from` application parameter. 38 | * 39 | * @param string|string[] $recipient one or more recipient addresses. 40 | * Optionally as array of the form `[$email => $name, ...]` 41 | * @param string $view name of the view file to render from `@app/mail` 42 | * @param string $layout the `htmlLayout` parameter. If `false`, a pure 43 | * text email without layout is rendered. 44 | * @param array $params additional view parameters 45 | * @return bool whether the mail was sent successfully 46 | */ 47 | public static function to($recipient, $view, $layout, $params = []) 48 | { 49 | $mailer = Yii::$app->mailer; 50 | $appParams = Yii::$app->params; 51 | if ($layout === false) { 52 | $mailer->textLayout = false; 53 | } else { 54 | $mailer->htmlLayout = $layout; 55 | } 56 | $catchAll = $appParams['mail.catchAll']; 57 | if (!empty($catchAll)) { 58 | Yii::info("Using catchAll email. Original recipient was ".print_r($recipient, true), __METHOD__); 59 | $recipient = $catchAll; 60 | } 61 | if (empty($recipient)) { 62 | return false; 63 | } 64 | try { 65 | $message = $mailer 66 | ->compose($layout === false ? ['text' => $view] : $view, $params) 67 | ->setTo($recipient); 68 | 69 | if (!$message->getFrom()) { 70 | $from = isset($params['from']) ? $params['from'] : $appParams['mail.from']; 71 | $message->setFrom($from); 72 | } 73 | 74 | $recipientString = print_r($recipient, true); 75 | if ($message->send()) { 76 | Yii::info("Sent $view to $recipientString", __METHOD__); 77 | return true; 78 | } else { 79 | Yii::warning("Failed to send $view to $recipientString", __METHOD__); 80 | return false; 81 | } 82 | } catch (Swift_SwiftException $e) { 83 | $type = get_class($e); 84 | $message = $e->getMessage(); 85 | $trace = $e->getTraceAsString(); 86 | Yii::warning("Swift exception $type:\n$message\n\n$trace", __METHOD__); 87 | } 88 | return false; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /mail/layouts/user.php: -------------------------------------------------------------------------------- 1 | 9 | beginPage() ?> 10 | 11 | 12 | 13 | 14 | <?= Html::encode($this->title) ?> 15 | head() ?> 16 | 17 | 18 | beginBody() ?> 19 | 20 | endBody() ?> 21 | 22 | 23 | endPage() ?> 24 | -------------------------------------------------------------------------------- /mail/user/confirm-email.php: -------------------------------------------------------------------------------- 1 | setSubject('Complete registration with My Application'); 12 | 13 | $url = Url::to(['/user/confirm-email', 14 | 'token' => $user->email_confirmation_token, 15 | ], true); 16 | ?> 17 | 18 | Hello username) ?>,
19 | 20 |

Follow the link below to complete your registration:

21 | 22 | 23 | -------------------------------------------------------------------------------- /mail/user/reset-password.php: -------------------------------------------------------------------------------- 1 | setSubject('Password Reset for My Application'); 12 | 13 | $url = Url::to(['user/reset-password', 14 | 'token' => $user->password_reset_token, 15 | ], true); 16 | ?> 17 | 18 | Hello username) ?>,
19 | 20 |

Follow the link below to reset your password:

21 | 22 | 23 | -------------------------------------------------------------------------------- /migrations/m150101_000000_init.php: -------------------------------------------------------------------------------- 1 | execute('ALTER SCHEMA DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci'); 10 | $this->execute('ALTER SCHEMA CHARACTER SET utf8 COLLATE utf8_general_ci'); 11 | 12 | $this->createTable('user', [ 13 | 'id' => $this->primaryKey(), 14 | 'username' => $this->string()->notNull(), 15 | 'is_email_verified' => $this->boolean()->notNull()->defaultValue(0), 16 | 'auth_key' => $this->char(32)->notNull(), 17 | 'password_hash' => $this->string()->notNull(), 18 | 'password_reset_token' => $this->string(), 19 | 'email_confirmation_token' => $this->string(), 20 | 'email' => $this->string()->notNull(), 21 | 'role' => $this->smallInteger()->notNull()->defaultValue(10), 22 | 'status' => $this->smallInteger()->notNull()->defaultValue(10), 23 | 'created_at' => Schema::TYPE_DATETIME . ' NOT NULL', 24 | 'updated_at' => Schema::TYPE_DATETIME . ' NOT NULL', 25 | ], 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB'); 26 | } 27 | 28 | public function down() 29 | { 30 | $this->dropTable('user'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /models/User.php: -------------------------------------------------------------------------------- 1 | self::STATUS_ACTIVE], 56 | ['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]], 57 | 58 | ['role', 'default', 'value' => self::ROLE_USER], 59 | ['role', 'in', 'range' => [self::ROLE_USER]], 60 | ]; 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function behaviors() 67 | { 68 | return [ 69 | 'timestamp' => [ 70 | 'class' => TimestampBehavior::className(), 71 | ], 72 | ]; 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function beforeSave($insert) 79 | { 80 | if ($this->isNewRecord) { 81 | $this->generateAuthKey(); 82 | $this->generateEmailConfirmationToken(); 83 | } 84 | 85 | return parent::beforeSave($insert); 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | */ 91 | public function afterSave($insert, $changedAttributes) 92 | { 93 | if ($insert && !Mail::toUser($this, 'confirm-email')) { 94 | Yii::warning('Failed to send confirmation email to new user.', __METHOD__); 95 | } 96 | parent::afterSave($insert, $changedAttributes); 97 | } 98 | 99 | /** 100 | * @inheritdoc 101 | */ 102 | public static function findIdentity($id) 103 | { 104 | return static::findOne($id); 105 | } 106 | 107 | /** 108 | * @inheritdoc 109 | */ 110 | public static function findIdentityByAccessToken($token, $type = null) 111 | { 112 | throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.'); 113 | } 114 | 115 | /** 116 | * @inheritdoc 117 | */ 118 | public function getId() 119 | { 120 | return $this->getPrimaryKey(); 121 | } 122 | 123 | /** 124 | * @inheritdoc 125 | */ 126 | public function getAuthKey() 127 | { 128 | return $this->auth_key; 129 | } 130 | 131 | /** 132 | * @inheritdoc 133 | */ 134 | public function validateAuthKey($authKey) 135 | { 136 | return $this->getAuthKey() === $authKey; 137 | } 138 | 139 | /** 140 | * Validates password 141 | * 142 | * @param string $password password to validate 143 | * @return boolean if password provided is valid for current user 144 | */ 145 | public function validatePassword($password) 146 | { 147 | return Yii::$app->security->validatePassword($password, $this->password_hash); 148 | } 149 | 150 | /** 151 | * Sets a new password or keeps the old password if the provided password 152 | * is empty. 153 | * 154 | * @param string $password 155 | */ 156 | public function setPassword($password) 157 | { 158 | $this->_password = $password; 159 | if (!empty($password)) { 160 | $this->password_hash = Yii::$app->security->generatePasswordHash($password); 161 | } 162 | } 163 | 164 | /** 165 | * @return string|null the current password value, if set from form. Null otherwise. 166 | */ 167 | public function getPassword() 168 | { 169 | return $this->_password; 170 | } 171 | 172 | /** 173 | * Generates "remember me" authentication key 174 | */ 175 | public function generateAuthKey() 176 | { 177 | $this->auth_key = Yii::$app->security->generateRandomString(); 178 | } 179 | 180 | /** 181 | * Generates new email confirmation token 182 | * @param bool $save whether to save the record. Default is `false`. 183 | * @return bool|null whether the save was successful or null if $save was false. 184 | */ 185 | public function generateEmailConfirmationToken($save = false) 186 | { 187 | $this->email_confirmation_token = Yii::$app->security->generateRandomString() . '_' . time(); 188 | if ($save) { 189 | return $this->save(); 190 | } 191 | } 192 | 193 | /** 194 | * Generates new password reset token 195 | * @param bool $save whether to save the record. Default is `false`. 196 | * @return bool|null whether the save was successful or null if $save was false. 197 | */ 198 | public function generatePasswordResetToken($save = false) 199 | { 200 | $this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time(); 201 | if ($save) { 202 | return $this->save(); 203 | } 204 | } 205 | 206 | /** 207 | * Resets to a new password and deletes the password reset token. 208 | * @param string $password the new password for this user. 209 | * @return bool whether the record was updated successfully 210 | */ 211 | public function resetPassword($password) 212 | { 213 | $this->setPassword($password); 214 | $this->password_reset_token = null; 215 | return $this->save(); 216 | } 217 | 218 | /** 219 | * Confirms an email an deletes the email confirmation token. 220 | * @return bool whether the record was updated successfully 221 | */ 222 | public function confirmEmail() 223 | { 224 | $this->email_confirmation_token = null; 225 | $this->is_email_verified = 1; 226 | return $this->save(); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /models/behaviors/TimestampBehavior.php: -------------------------------------------------------------------------------- 1 | ['created_at', 'updated_at'], 12 | ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'], 13 | ]; 14 | 15 | public function init() 16 | { 17 | parent::init(); 18 | $this->value = new Expression('NOW()'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /models/forms/LoginForm.php: -------------------------------------------------------------------------------- 1 | hasErrors()) { 42 | $user = $this->getUser(); 43 | 44 | if (!$user || !$user->validatePassword($this->password)) { 45 | $this->addError('password', 'Incorrect username or password.'); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * Logs in a user using the provided username and password. 52 | * @return boolean whether the user is logged in successfully 53 | */ 54 | public function login() 55 | { 56 | if ($this->validate()) { 57 | return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0); 58 | } else { 59 | return false; 60 | } 61 | } 62 | 63 | /** 64 | * Finds user by [[username]] 65 | * 66 | * @return User|null 67 | */ 68 | public function getUser() 69 | { 70 | if ($this->_user === false) { 71 | $this->_user = User::find()->canLogin()->username($this->username)->one(); 72 | } 73 | 74 | return $this->_user; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /models/forms/PasswordResetRequestForm.php: -------------------------------------------------------------------------------- 1 | 'trim'], 23 | ['email', 'required'], 24 | ['email', 'email'], 25 | ['email', 'exist', 26 | 'targetClass' => '\app\models\User', 27 | 'filter' => ['status' => User::STATUS_ACTIVE], 28 | 'message' => 'There is no user with such email.' 29 | ], 30 | ]; 31 | } 32 | 33 | /** 34 | * Sends an email with a link, for resetting the password. 35 | * 36 | * @return bool whether the email was sent 37 | */ 38 | public function sendEmail() 39 | { 40 | if (!$this->validate()) { 41 | return false; 42 | } 43 | 44 | /** @var User $user */ 45 | $user = User::find() 46 | ->canLogin() 47 | ->email($this->email) 48 | ->one(); 49 | 50 | if ($user && $user->generatePasswordResetToken(true) && Mail::toUser($user, 'reset-password')) { 51 | return true; 52 | } 53 | 54 | $this->addError('email', 'We can not reset the password for this user'); 55 | return false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /models/forms/ResetPasswordForm.php: -------------------------------------------------------------------------------- 1 | _user = User::find() 34 | ->canLogin() 35 | ->passwordResetToken($token) 36 | ->one(); 37 | if (!$this->_user) { 38 | throw new InvalidParamException('Wrong password reset token.'); 39 | } 40 | parent::__construct($config); 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public function rules() 47 | { 48 | return [ 49 | ['password', 'required'], 50 | ['password', 'string', 'min' => 6], 51 | ]; 52 | } 53 | 54 | /** 55 | * Resets password. 56 | * 57 | * @return bool whether password was reset. 58 | */ 59 | public function resetPassword() 60 | { 61 | if (!$this->validate()) { 62 | return false; 63 | } 64 | return $this->_user->resetPassword($this->password); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /models/forms/SignupForm.php: -------------------------------------------------------------------------------- 1 | 'trim'], 27 | ['username', 'unique', 'targetClass' => User::className()], 28 | ['username', 'string', 'min' => 2, 'max' => 255], 29 | 30 | ['email', 'filter', 'filter' => 'trim'], 31 | ['email', 'email'], 32 | ['email', 'unique', 'targetClass' => User::className() ], 33 | 34 | ['password', 'string', 'min' => 6], 35 | ]; 36 | } 37 | 38 | /** 39 | * Signs up new user 40 | * 41 | * @return app\models\User|null the saved user model or null if saving fails 42 | */ 43 | public function signup() 44 | { 45 | if (!$this->validate()) { 46 | return null; 47 | } 48 | $user = new User($this->attributes); 49 | if ($user->save()) { 50 | Mail::toUser($user, 'confirm-email'); 51 | return $user; 52 | } else { 53 | $errors = print_r($user->errors, true); 54 | Yii::warning("Could not save new user:\n$errors", __METHOD__); 55 | return null; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /models/queries/UserQuery.php: -------------------------------------------------------------------------------- 1 | andWhere([ 15 | 'status' => User::STATUS_ACTIVE, 16 | 'is_email_verified' => 1, 17 | ]); 18 | } 19 | 20 | /** 21 | * @return static the query with condition for given email applied 22 | */ 23 | public function email($email) 24 | { 25 | return $this->andWhere(['email' => $email]); 26 | } 27 | 28 | /** 29 | * @return static the query with condition for given username applied 30 | */ 31 | public function username($username) 32 | { 33 | return $this->andWhere(['username' => $username]); 34 | } 35 | 36 | /** 37 | * @param string $token the password reset token 38 | * @return static the query with conditions for valid password reset token applied 39 | */ 40 | public function passwordResetToken($token) 41 | { 42 | $expire = \Yii::$app->params['user.passwordResetTokenExpire']; 43 | $parts = explode('_', $token); 44 | $timestamp = (int) end($parts); 45 | if ($timestamp + $expire < time()) { 46 | // token expired 47 | return $this->andWhere('FALSE'); 48 | } 49 | return $this->andWhere(['password_reset_token' => $token]); 50 | } 51 | 52 | /** 53 | * @param string $token the email confirmation token 54 | * @return static the query with conditions for valid email confirmation token applied 55 | */ 56 | public function emailConfirmationToken($token) 57 | { 58 | $expire = \Yii::$app->params['user.emailConfirmationTokenExpire']; 59 | $parts = explode('_', $token); 60 | $timestamp = (int) end($parts); 61 | if ($timestamp + $expire < time()) { 62 | // token expired 63 | return $this->andWhere('FALSE'); 64 | } 65 | return $this->andWhere(['email_confirmation_token' => $token]); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /var/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /views/layouts/main.php: -------------------------------------------------------------------------------- 1 | 13 | beginPage() ?> 14 | 15 | 16 | 17 | 18 | 19 | 20 | <?= Html::encode($this->title) ?> 21 | head() ?> 22 | 23 | 24 | beginBody() ?> 25 |
26 | 'My Company', 29 | 'brandUrl' => Yii::$app->homeUrl, 30 | 'options' => [ 31 | 'class' => 'navbar-inverse navbar-fixed-top', 32 | ], 33 | ]); 34 | $menuItems = [ 35 | ['label' => 'Home', 'url' => ['/site/index']], 36 | ]; 37 | if (Yii::$app->user->isGuest) { 38 | $menuItems[] = ['label' => 'Signup', 'url' => ['/user/signup']]; 39 | $menuItems[] = ['label' => 'Login', 'url' => ['/user/login']]; 40 | } else { 41 | $menuItems[] = [ 42 | 'label' => 'Logout (' . Yii::$app->user->identity->username . ')', 43 | 'url' => ['/user/logout'], 44 | 'linkOptions' => ['data-method' => 'post'] 45 | ]; 46 | } 47 | echo Nav::widget([ 48 | 'options' => ['class' => 'navbar-nav navbar-right'], 49 | 'items' => $menuItems, 50 | ]); 51 | NavBar::end(); 52 | ?> 53 | 54 |
55 | isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], 57 | ]) ?> 58 | 59 |
60 |
61 | 62 | 68 | 69 | endBody() ?> 70 | 71 | 72 | endPage() ?> 73 | -------------------------------------------------------------------------------- /views/site/error.php: -------------------------------------------------------------------------------- 1 | title = $name; 10 | ?> 11 |
12 | 13 |

title) ?>

14 | 15 |
16 | 17 |
18 | 19 |

20 | The above error occurred while the Web server was processing your request. 21 |

22 |

23 | Please contact us if you think this is a server error. Thank you. 24 |

25 | 26 |
27 | -------------------------------------------------------------------------------- /views/site/index.php: -------------------------------------------------------------------------------- 1 | title = 'My Yii Application'; 4 | ?> 5 |
6 | 7 |
8 |

Congratulations!

9 | 10 |

You have successfully created your Yii-powered application.

11 | 12 |

Get started with Yii

13 |
14 | 15 |
16 | 17 |
18 |
19 |

Heading

20 | 21 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et 22 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip 23 | ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 24 | fugiat nulla pariatur.

25 | 26 |

Yii Documentation »

27 |
28 |
29 |

Heading

30 | 31 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et 32 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip 33 | ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 34 | fugiat nulla pariatur.

35 | 36 |

Yii Forum »

37 |
38 |
39 |

Heading

40 | 41 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et 42 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip 43 | ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 44 | fugiat nulla pariatur.

45 | 46 |

Yii Extensions »

47 |
48 |
49 | 50 |
51 |
52 | -------------------------------------------------------------------------------- /views/site/maintenance.php: -------------------------------------------------------------------------------- 1 | context->layout = false; 4 | ?> 5 |

Maintenance

6 | 7 |

We are sorry, but the application is currently being maintained. 8 | Please try again later.

9 | -------------------------------------------------------------------------------- /views/user/confirmed-email.php: -------------------------------------------------------------------------------- 1 | title = 'Signup'; 8 | $this->params['breadcrumbs'][] = $this->title; 9 | ?> 10 |
11 |

Email Confirmed

12 | 13 |

You successfully confirmed your email address and can now 14 | . 15 |

16 | 17 |
18 | -------------------------------------------------------------------------------- /views/user/email-confirmation-failed.php: -------------------------------------------------------------------------------- 1 | title = 'Signup'; 8 | $this->params['breadcrumbs'][] = $this->title; 9 | $params = Yii::$app->params; 10 | ?> 11 |
12 |

Could not complete registration

13 | 14 |

You either supplied an invalid confirmation link or the link has meanwhile expired.

15 |

16 | 17 |
18 | -------------------------------------------------------------------------------- /views/user/login.php: -------------------------------------------------------------------------------- 1 | title = 'Login'; 10 | $this->params['breadcrumbs'][] = $this->title; 11 | ?> 12 |
13 |

title) ?>

14 | 15 |

Please fill out the following fields to login:

16 | 17 |
18 |
19 | 'login-form']); ?> 20 | field($model, 'username') ?> 21 | field($model, 'password')->passwordInput() ?> 22 | field($model, 'rememberMe')->checkbox() ?> 23 |
24 | If you forgot your password you can . 25 |
26 |
27 | 'btn btn-primary', 'name' => 'login-button']) ?> 28 |
29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /views/user/password-was-reset.php: -------------------------------------------------------------------------------- 1 | title = 'Signup'; 8 | $this->params['breadcrumbs'][] = $this->title; 9 | ?> 10 |
11 |

Password Was Reset

12 | 13 |

You can now with your new password.

14 | 15 |
16 | -------------------------------------------------------------------------------- /views/user/request-password-reset.php: -------------------------------------------------------------------------------- 1 | title = 'Request password reset'; 10 | $this->params['breadcrumbs'][] = $this->title; 11 | ?> 12 |
13 |

title) ?>

14 | 15 |

Please fill out your email. A link to reset password will be sent there.

16 | 17 |
18 |
19 | 'request-password-reset-form']); ?> 20 | field($model, 'email') ?> 21 |
22 | 'btn btn-primary']) ?> 23 |
24 | 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /views/user/requested-password-reset.php: -------------------------------------------------------------------------------- 1 | title = 'Signup'; 8 | $this->params['breadcrumbs'][] = $this->title; 9 | ?> 10 |
11 |

Thank You

12 | 13 |

We have sent you an email with a reset link. Please check your Inbox.

14 | 15 |
16 | -------------------------------------------------------------------------------- /views/user/reset-password.php: -------------------------------------------------------------------------------- 1 | title = 'Reset password'; 10 | $this->params['breadcrumbs'][] = $this->title; 11 | ?> 12 |
13 |

title) ?>

14 | 15 |

Please choose your new password:

16 | 17 |
18 |
19 | 'reset-password-form']); ?> 20 | field($model, 'password')->passwordInput() ?> 21 |
22 | 'btn btn-primary']) ?> 23 |
24 | 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /views/user/signed-up.php: -------------------------------------------------------------------------------- 1 | title = 'Signup'; 8 | $this->params['breadcrumbs'][] = $this->title; 9 | ?> 10 |
11 |

Thank You

12 | 13 |

We have sent you an email with a confirmation link. Please check your Inbox.

14 | 15 |
16 | -------------------------------------------------------------------------------- /views/user/signup.php: -------------------------------------------------------------------------------- 1 | title = 'Signup'; 10 | $this->params['breadcrumbs'][] = $this->title; 11 | ?> 12 |
13 |

title) ?>

14 | 15 |

Please fill out the following fields to signup:

16 | 17 |
18 |
19 | 'form-signup']); ?> 20 | field($model, 'username') ?> 21 | field($model, 'email') ?> 22 | field($model, 'password')->passwordInput(['autocomplete' => 'new-password']) ?> 23 |
24 | 'btn btn-primary', 'name' => 'signup-button']) ?> 25 |
26 | 27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /web/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /web/css/site.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | .wrap { 7 | min-height: 100%; 8 | height: auto; 9 | margin: 0 auto -60px; 10 | padding: 0 0 60px; 11 | } 12 | 13 | .wrap > .container { 14 | padding: 70px 15px 20px; 15 | } 16 | 17 | .footer { 18 | height: 60px; 19 | background-color: #f5f5f5; 20 | border-top: 1px solid #ddd; 21 | padding-top: 20px; 22 | } 23 | 24 | .jumbotron { 25 | text-align: center; 26 | background-color: transparent; 27 | } 28 | 29 | .jumbotron .btn { 30 | font-size: 21px; 31 | padding: 14px 24px; 32 | } 33 | 34 | .not-set { 35 | color: #c55; 36 | font-style: italic; 37 | } 38 | 39 | /* add sorting icons to gridview sort links */ 40 | a.asc:after, a.desc:after { 41 | position: relative; 42 | top: 1px; 43 | display: inline-block; 44 | font-family: 'Glyphicons Halflings'; 45 | font-style: normal; 46 | font-weight: normal; 47 | line-height: 1; 48 | padding-left: 5px; 49 | } 50 | 51 | a.asc:after { 52 | content: /*"\e113"*/ "\e151"; 53 | } 54 | 55 | a.desc:after { 56 | content: /*"\e114"*/ "\e152"; 57 | } 58 | 59 | .sort-numerical a.asc:after { 60 | content: "\e153"; 61 | } 62 | 63 | .sort-numerical a.desc:after { 64 | content: "\e154"; 65 | } 66 | 67 | .sort-ordinal a.asc:after { 68 | content: "\e155"; 69 | } 70 | 71 | .sort-ordinal a.desc:after { 72 | content: "\e156"; 73 | } 74 | 75 | .grid-view th { 76 | white-space: nowrap; 77 | } 78 | 79 | .hint-block { 80 | display: block; 81 | margin-top: 5px; 82 | color: #999; 83 | } 84 | 85 | .error-summary { 86 | color: #a94442; 87 | background: #fdf7f7; 88 | border-left: 3px solid #eed3d7; 89 | padding: 10px 20px; 90 | margin: 0 0 15px 0; 91 | } 92 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | web()])->run(); 10 | -------------------------------------------------------------------------------- /yii: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | console()]); 15 | 16 | exit($application->run()); 17 | -------------------------------------------------------------------------------- /yii.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem ------------------------------------------------------------- 4 | rem Yii command line bootstrap script for Windows. 5 | rem 6 | rem @author Qiang Xue 7 | rem @link http://www.yiiframework.com/ 8 | rem @copyright Copyright © 2012 Yii Software LLC 9 | rem @license http://www.yiiframework.com/license/ 10 | rem ------------------------------------------------------------- 11 | 12 | @setlocal 13 | 14 | set YII_PATH=%~dp0 15 | 16 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe 17 | 18 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 19 | 20 | @endlocal 21 | --------------------------------------------------------------------------------