Compare commits

...

10 Commits

Author SHA1 Message Date
c6212165af
Вынес константы 2023-02-25 21:30:54 +03:00
0cb732c803
Изменил деплой
Вместо deployer - fabric/invoke
2022-08-15 15:45:57 +03:00
2c4ca426fd
Обновил php-cs-fixer 2022-08-15 12:47:09 +03:00
c8da8fa23f
Обновил зависимости 2022-08-15 12:39:51 +03:00
6a7047cfe9
Добавил деплой в продакшен через docker
- Создание образа
- Запись в реестре
- Развертывание из реестра на сервере
2022-08-15 12:15:37 +03:00
7007b184d0
Обновил сборку и зависимости 2021-07-18 10:44:03 +03:00
91cfa35d2c Исправил заголовок заметки 2020-11-09 10:22:23 +03:00
da98a36142 Обновил js зависимости 2020-11-08 12:14:39 +03:00
d37b3473a4 Написал короткую заметку про nullable поля 2020-11-08 12:03:45 +03:00
7676933e40 Обновил php, composer
И сделал косметические правки бандлов
2020-11-08 11:18:56 +03:00
31 changed files with 11928 additions and 3205 deletions

View File

@ -30,7 +30,7 @@ jobs:
- attach_workspace:
at: /tmp/workspace
- checkout
- run: docker/provision.sh
- run: docker/php/provision.sh
- run: composer install --no-interaction --no-progress
- run: mkdir -p ${STATIC_DIR}
- run: cp -R /tmp/workspace/static/* ${STATIC_DIR}
@ -55,7 +55,7 @@ jobs:
- add_ssh_keys
- run: apt-get update; apt-get install -yy openssh-client
- run: ssh-keyscan "vakhrushev.me" >> ~/.ssh/known_hosts
- run: docker/provision.sh
- run: docker/php/provision.sh
- attach_workspace:
at: /tmp/workspace
- run: mkdir -p ./output_prod

View File

@ -1,4 +1,3 @@
/node_modules
/output_*
/var
/vendor

3
.env
View File

@ -1,2 +1,3 @@
PROJECT=homepage
PHP_IMAGE=homepage-php
NODE_IMAGE=node:12-alpine
NODE_IMAGE=homepage-node

2
.gitignore vendored
View File

@ -1,7 +1,9 @@
.idea/
.vscode/
output_*
node_modules/
var/
vendor/
.php_cs.cache
.php-cs-fixer.cache

View File

@ -7,7 +7,7 @@ $finder = PhpCsFixer\Finder::create()
->in(__DIR__.'/bundle')
;
return PhpCsFixer\Config::create()
return (new PhpCsFixer\Config())
->setFinder($finder)
->setRules([
'@Symfony' => true,

View File

@ -70,7 +70,7 @@ watch: clean build-assets
# Deploy
deploy: build-prod
./tools/dep deploy production -vv
invoke deploy
rollback:
./tools/dep rollback production -vv

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Homepage\HtmlPrettierBundle\HtmlPrettierBundle;
use Homepage\SiteMapBundle\SiteMapBundle;
use Homepage\TwigExtensionBundle\TwigExtensionBundle;
@ -12,7 +14,7 @@ class SculpinKernel extends AbstractKernel
return [
TwigExtensionBundle::class,
SiteMapBundle::class,
HtmlPrettierBundle::class,
HtmlPrettierBundle::class,
];
}
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\HtmlPrettierBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\HtmlPrettierBundle;
use Generator;
@ -7,7 +9,9 @@ use Sculpin\Core\Event\SourceSetEvent;
use Sculpin\Core\Sculpin;
use Sculpin\Core\Source\SourceInterface;
use Sculpin\Core\Source\SourceSet;
use function strlen;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class HtmlPrettier implements EventSubscriberInterface

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\HtmlPrettierBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\SiteMapBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\SiteMapBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\SiteMapBundle;
use Sculpin\Core\DataProvider\DataProviderInterface;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\TwigExtensionBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\TwigExtensionBundle;
use Twig\Extension\AbstractExtension;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\TwigExtensionBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;

View File

@ -9,16 +9,20 @@
}
],
"require": {
"php": "~7.3",
"php": "~7.4",
"ext-tidy": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.15",
"sculpin/sculpin": "^3.0"
},
"autoload": {
"psr-4": {
"Homepage\\": "bundle/"
}
},
"config": {
"allow-plugins": {
"sculpin/sculpin-theme-composer-plugin": true
}
}
}

1736
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +0,0 @@
<?php
namespace Deployer;
require 'recipe/common.php';
host('vakhrushev.me')
->user('homepage')
->stage('production')
->set('deploy_path', '/var/www/homepage')
;
host('192.168.50.10')
->stage('test')
->user('homepage')
->set('deploy_path', '/var/www/homepage')
->addSshOption('UserKnownHostsFile', '/dev/null')
->addSshOption('StrictHostKeyChecking', 'no')
;
// Saved releases
set('keep_releases', 2);
// Excluded dirs for upload
set('upload_excluded_dirs', []);
// Upload app sources on remote host
task('upload', function () {
$excluded = array_map(function ($dir) {
return sprintf('--exclude "%s"', $dir);
}, get('upload_excluded_dirs'));
upload(__DIR__ . '/output_prod/', '{{release_path}}', [
'options' => $excluded,
]);
});
// Deploy task
task('deploy', [
'deploy:info',
'deploy:prepare',
'deploy:lock',
'deploy:release',
'upload',
'deploy:symlink',
'deploy:unlock',
'cleanup',
]);
after('deploy', 'success');

View File

@ -1,7 +0,0 @@
FROM php:7.4.7-cli
COPY ./docker/provision.sh /opt/
RUN /opt/provision.sh
WORKDIR /srv/app

View File

@ -0,0 +1,3 @@
FROM nginx:stable
COPY output_prod /usr/share/nginx/html

View File

@ -0,0 +1,11 @@
version: '2'
services:
nginx:
image: '${NGINX_IMAGE}'
# user: '${CURRENT_UID}:${CURRENT_GID}'
restart: unless-stopped
ports:
- '${WEB_SERVER_PORT}:80'
env_file:
- .env

3
docker/node/Dockerfile Normal file
View File

@ -0,0 +1,3 @@
FROM node:12.22.3-alpine
RUN npm install -g npm

7
docker/php/Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM php:7.4.21-cli
COPY ./docker/php/provision.sh /opt/
RUN /opt/provision.sh
WORKDIR /srv/app

View File

@ -19,11 +19,12 @@ docker-php-ext-install tidy \
mkdir -p /srv/app
# Composer and required tools
curl -sLO https://getcomposer.org/download/1.10.8/composer.phar \
curl -sLO https://getcomposer.org/download/2.3.10/composer.phar \
&& mv composer.phar /usr/local/bin/composer \
&& chmod +x /usr/local/bin/composer
# Deployer
curl -sLO https://deployer.org/releases/v6.8.0/deployer.phar \
&& mv deployer.phar /usr/local/bin/dep \
&& chmod +x /usr/local/bin/dep
# PHP-CS-Fixer
curl -sLO https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v3.9.5/php-cs-fixer.phar \
&& mv php-cs-fixer.phar /usr/local/bin/php-cs-fixer \
&& chmod +x /usr/local/bin/php-cs-fixer

12990
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,27 +6,27 @@
"description": "Homepage",
"devDependencies": {
"@anwinged/predictor": "^0.2.1",
"@babel/core": "^7.10.3",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-transform-runtime": "^7.10.3",
"@babel/preset-env": "^7.10.3",
"@babel/runtime": "^7.10.3",
"autoprefixer": "^9.8.4",
"babel-loader": "^8.1.0",
"@babel/core": "^7.14.6",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.7",
"@babel/runtime": "^7.14.6",
"autoprefixer": "^9.8.6",
"babel-loader": "^8.2.2",
"css-loader": "^2.1.1",
"glob": "^7.1.6",
"glob": "^7.1.7",
"mini-css-extract-plugin": "^0.6.0",
"node-sass": "^4.14.1",
"postcss-loader": "^3.0.0",
"prettier": "^1.19.1",
"sass-loader": "^7.3.1",
"style-loader": "^0.23.1",
"underscore": "^1.10.2",
"vue": "^2.6.11",
"vue-loader": "^15.9.3",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.43.0",
"underscore": "^1.13.1",
"vue": "^2.6.14",
"vue-loader": "^15.9.7",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.14",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12"
},
"scripts": {

View File

@ -0,0 +1,177 @@
---
title: Организация доступа к nullable полям класса
description: Заметка о том, лучше организовать доступ к полям класса, которые могут содержать значение null
keywords: [чистый код, php, null, поля класса]
---
Нередкая ситуация, когда в классе есть поле, которое может содержать `null`.
```php
class User
{
private Email $email;
private ?string $name;
}
```
Пользователь может указать имя, а может и не указывать,
ограничившись только почтовым адресом.
А далее мы пишем код, которые работает с полем имени.
```php
class User
{
private ?string $name;
public function hasName(): bool
{
return $this->name !== null;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
```
И использование этого кода:
```php
/** @var User $user */
if ($user->hasName()) {
do_something_with_name($user->getName());
}
function do_something_with_name(string $name) {}
```
Выглядит хорошо.
Сначала убедились, что имя установлено, а потом использовали его.
Но статический анализатор нам обязательно припомнит, что мы пытаемся передать
в функцию `do_something_with_name` значение типа `string|null`, хотя функция
ожидает значение типа `string`.
И получается дурацкая ситуация, что формально мы должны дописать
еще одну проверку.
```php
/** @var User $user */
if ($user->hasName()) {
$name = $user->getName();
if ($name !== null) {
do_something_with_name($name);
}
}
function do_something_with_name(string $name) {}
```
Статический анализатор наш друг, он помогает находить ошибки и несоответствия
в коде.
И здесь он нашел такое формальное несоответствие типов.
Статический анализатор прав, а мы, как проектировщики интерфейса, не правы.
На самом деле мы смешали два подхода, когда описывали методы в нашем классе:
1. Получить и проверить
2. Проверить и получить
## Получить и проверить
И сразу начнем с примера использования.
```php
$name = $user->getName();
if ($name !== null) {
do_something_with_name($name);
}
```
Сначала мы получаем значение поля, а потом проверяем, соответствует ли это
значение нашим требованиям. Класс при этом будет построен вот так:
```php
class User
{
private ?string $name;
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
```
Заметьте, здесь уже нет метода `hasName()`, потому что этот метод перестал быть
нужным. Его роль исполняет метод `getName()`.
## Проверить и получить
Второй подход: сначала проверяем значение, а потом работаем с ним:
```php
if ($user->hasName()) {
do_something_with_name($user->getName());
}
```
Структура класса:
```php
class User
{
private ?string $name;
public function hasName(): bool
{
return $this->name !== null;
}
public function getName(): string
{
if ($this->name === null) {
throw new \LogicException('Name is not set');
}
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
```
Смотрите отличия.
Метод `hasName()` остается.
А вот метод `getName()` теперь возвращает значение типа `string`.
Он выбросит исключение, если мы попытаемся получить значение,
которое не установлено.
## Использование
Теперь встает вопрос, когда и какой подход следует использовать.
- Если ситуация, когда поле не установлено, скорее исключительная, нежели
обычная, то можно использовать второй подход, а проверку опустить.
Исключение в методе `getName()` позволит обнаружить странное поведение.
- Если в пустом поле нет ничего не обычного, то подход "получить и проверить"
будет удобнее, все равно нужно делать проверку.
В любом случае, нужно смотреть на уместность того или иного подхода в каждом
случае, и не использовать их одновременно.

49
tasks.py Normal file
View File

@ -0,0 +1,49 @@
from fabric import Connection
from invoke import task
from datetime import datetime
import subprocess
import shlex
APP_NAME = "homepage"
SSH_HOST = "homepage@51.250.85.23"
DOCKER_REGISTRY = "cr.yandex/crplfk0168i4o8kd7ade"
def run(args):
return subprocess.run(args, check=True, capture_output=True).stdout
@task
def deploy(c):
timestamp = int(datetime.now().timestamp())
commit = run(["git", "rev-parse", "--short", "HEAD"]).decode("utf-8").strip()
nginx_image_tag = f"{DOCKER_REGISTRY}/homepage-nginx:{commit}-{timestamp}"
print(f"Build nginx image {nginx_image_tag}")
run(
[
"docker",
"build",
"--file",
"docker/Dockerfile.nginx.prod",
"--tag",
nginx_image_tag,
".",
]
)
print("Push nginx image")
run(["docker", "push", nginx_image_tag])
print("Ready to setup remote host")
with Connection(SSH_HOST) as c:
c.put(
"./docker/docker-compose.prod.yml",
remote="/home/homepage/docker-compose.yml",
)
c.run("cp .env .env.prod")
c.run(f"echo NGINX_IMAGE={shlex.quote(nginx_image_tag)} >> .env.prod")
c.run(f"docker-compose --project-name {shlex.quote(APP_NAME)} --env-file=.env.prod up --detach")

View File

@ -1,8 +1,15 @@
#!/bin/bash
set -eu
source .env
docker build \
--file docker/Dockerfile \
--file docker/php/Dockerfile \
--tag "${PHP_IMAGE}" \
"$PWD"
docker build \
--file docker/node/Dockerfile \
--tag "${NODE_IMAGE}" \
"$PWD"

View File

@ -6,8 +6,7 @@ docker run \
--rm \
--interactive \
--tty \
--init \
--user "$UID:$(id -g)" \
--volume="$PWD:/srv/app" \
"${PHP_IMAGE}" \
./vendor/bin/php-cs-fixer "$@"
--volume "$PWD:/srv/app" \
"${PHP_IMAGE}" \
php-cs-fixer "$@"