Compare commits

...

24 Commits

Author SHA1 Message Date
f797e1718b Remov old stale pages 2025-04-13 17:08:20 +03:00
19c2801856 Fix undefined key in array 2025-04-13 17:07:42 +03:00
63a324c437 Remove social links 2025-04-13 17:01:05 +03:00
acb99ca6f1 Remove old docker-compose file 2025-04-13 11:10:34 +03:00
3b001cc2fb Change git repositories url 2025-04-13 11:06:37 +03:00
f523f6c39a Change deploy to use ansible 2025-04-13 11:06:20 +03:00
fdb82bddf5 Add production image build 2025-04-12 20:14:03 +03:00
7482786c22 Remove old tooling 2025-04-12 19:51:28 +03:00
b9cc69c160 Rewrite build with task 2025-04-12 19:43:55 +03:00
3d26e338e2 Remove old artefacts 2024-12-25 11:54:08 +03:00
0ea97e907e Refactor format actions 2024-12-25 11:47:47 +03:00
786efb4503 Refactoring install actions 2024-12-25 11:27:05 +03:00
e4b27c55bf Move image building to taskfile 2024-12-25 10:32:08 +03:00
221d408ae3 Add taskfile 2024-12-25 10:31:47 +03:00
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
45 changed files with 3663 additions and 4655 deletions

View File

@ -1,80 +0,0 @@
version: 2.1
jobs:
build_assets:
docker:
- image: node:12-alpine
environment:
OUTPUT_DIR: ./output_prod/static
steps:
- checkout
- run: npm ci
- run: mkdir -p ${OUTPUT_DIR}
- run: npm run-script build-prod
- run: ls -la ${OUTPUT_DIR}
- run: mkdir -p /tmp/workspace/static
- run: cp -R ${OUTPUT_DIR}/* /tmp/workspace/static
- persist_to_workspace:
root: /tmp/workspace
paths:
- static/*
build_html:
docker:
- image: php:7.4-cli
environment:
APP_ENV: prod
APP_URL: https://vakhrushev.me
STATIC_DIR: ./output_prod/static
steps:
- attach_workspace:
at: /tmp/workspace
- checkout
- run: docker/provision.sh
- run: composer install --no-interaction --no-progress
- run: mkdir -p ${STATIC_DIR}
- run: cp -R /tmp/workspace/static/* ${STATIC_DIR}
- run: |
./vendor/bin/sculpin generate \
--env="${APP_ENV}" \
--url="${APP_URL}" \
--no-interaction \
-vv
- run: mkdir -p /tmp/workspace/html
- run: cp -R ./output_prod/* /tmp/workspace/html
- persist_to_workspace:
root: /tmp/workspace
paths:
- html/*
deploy:
docker:
- image: php:7.4-cli
steps:
- checkout
- 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
- attach_workspace:
at: /tmp/workspace
- run: mkdir -p ./output_prod
- run: cp -R /tmp/workspace/html/* ./output_prod
- run: ls -la ./output_prod
- run: ls -la ./output_prod/static
- run: dep deploy production -vv
workflows:
version: 2
build_and_deploy:
jobs:
- build_assets:
filters:
branches:
only: master
- build_html:
requires:
- build_assets
- deploy:
requires:
- build_html

View File

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

View File

@ -6,10 +6,7 @@ insert_final_newline = true
indent_style = space
indent_size = 4
[*.{html,twig,yml,xml}]
indent_size = 2
[gulpfile.js]
[*.{html,twig,yml,yaml,xml}]
indent_size = 2
[package.json]

2
.env
View File

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

6
.gitignore vendored
View File

@ -1,7 +1,13 @@
.idea/
.vscode/
.cache/
.config/
.home/
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

@ -1,76 +0,0 @@
APP_ENV := dev
APP_OUTPUT_DIR := output_dev
APP_URL := homepage.site
APP_NPM_BUILD_CMD := build
ifeq ($(TARGET), prod)
APP_ENV := prod
APP_OUTPUT_DIR := output_prod
APP_URL := https://vakhrushev.me
APP_NPM_BUILD_CMD := build-prod
endif
# Installation
install: build-docker install-php-deps install-js-deps
build-docker:
./tools/build-docker
install-php-deps:
./tools/composer install --no-interaction
install-js-deps:
./tools/npm ci
# Building
clean:
rm -rf ./${APP_OUTPUT_DIR}/*
build-assets:
./tools/npm run "${APP_NPM_BUILD_CMD}"
build-site:
./tools/sculpin generate \
--env="${APP_ENV}" \
--url="${APP_URL}" \
--no-interaction \
-vv
build: clean build-assets build-site
build-prod:
$(MAKE) build TARGET=prod
# Format
format-pages:
./tools/npm run format-md
format-assets:
./tools/npm run format-webpack
./tools/npm run format-js
./tools/npm run format-vue
./tools/npm run format-style
format-php:
./tools/php-cs-fixer fix
format: format-pages format-assets format-php
watch: clean build-assets
./tools/sculpin generate \
--env="${APP_ENV}" \
--watch \
--server \
--port=8000 \
--no-interaction
# Deploy
deploy: build-prod
./tools/dep deploy production -vv
rollback:
./tools/dep rollback production -vv

View File

@ -1,10 +1,10 @@
Source for [vakhrushev.me](http://vakhrushev.me).
Source for [vakhrushev.me](https://vakhrushev.me).
Build:
make build-docker
make build
task build-docker
task build
Deploy:
make deploy
task deploy

130
Taskfile.yml Normal file
View File

@ -0,0 +1,130 @@
# https://taskfile.dev
version: '3'
vars:
USER_ID:
sh: id -u
GROUP_ID:
sh: id -g
PROJECT: "homepage"
PHP_IMAGE: "{{.PROJECT}}-php"
NODE_IMAGE: "{{.PROJECT}}-node"
DOCKER_COMMON_OPTS: >-
--rm
--interactive
--tty
--user {{.USER_ID}}:{{.GROUP_ID}}
--volume /etc/passwd:/etc/passwd:ro
--volume /etc/group:/etc/group:ro
--volume "./:/srv/app"
--workdir "/srv/app"
-e XDG_CONFIG_HOME=/srv/app/.config
-e XDG_CACHE_HOME=/srv/app/.cache
-e HOME=/srv/app/.home
tasks:
build-docker:
cmds:
- docker build --file docker/php/Dockerfile --tag "{{.PHP_IMAGE}}" .
- docker build --file docker/node/Dockerfile --tag "{{.NODE_IMAGE}}" .
composer:
cmds:
- docker run {{.DOCKER_COMMON_OPTS}} "{{.PHP_IMAGE}}" composer {{.CLI_ARGS}}
npm:
cmds:
- docker run {{.DOCKER_COMMON_OPTS}} "{{.NODE_IMAGE}}" npm {{.CLI_ARGS}}
sculpin:
cmds:
- docker run {{.DOCKER_COMMON_OPTS}} "{{.PHP_IMAGE}}" ./vendor/bin/sculpin {{.CLI_ARGS}}
shell-node:
cmds:
- docker run {{.DOCKER_COMMON_OPTS}} "{{.NODE_IMAGE}}" bash
install-dependencies:
cmds:
- task: composer
vars: { CLI_ARGS: "install" }
- task: npm
vars: { CLI_ARGS: "install" }
format-pages:
cmds:
- task: npm
vars: { CLI_ARGS: 'run format-md' }
format-assets:
cmds:
- task: npm
vars: { CLI_ARGS: 'run format-webpack' }
- task: npm
vars: { CLI_ARGS: 'run format-js' }
- task: npm
vars: { CLI_ARGS: 'run format-vue' }
- task: npm
vars: { CLI_ARGS: 'run format-style' }
format-php:
cmds:
- docker run {{.DOCKER_COMMON_OPTS}} "{{.PHP_IMAGE}}" php-cs-fixer fix
build-dev:
vars:
APP_OUTPUT_DIR: output_dev
NPM_SCRIPT: build
APP_ENV: dev
APP_URL: homepage.site
cmds:
- rm -rf ./{{.APP_OUTPUT_DIR}}/*
- task: npm
vars: { CLI_ARGS: 'run {{.NPM_SCRIPT}}' }
- task: sculpin
vars: { CLI_ARGS: 'generate --env="{{.APP_ENV}}" --url="{{.APP_URL}}" --no-interaction -vv' }
build-prod:
vars:
APP_OUTPUT_DIR: output_prod
NPM_SCRIPT: build-prod
APP_ENV: prod
APP_URL: https://vakhrushev.me
cmds:
- rm -rf ./{{.APP_OUTPUT_DIR}}/*
- task: npm
vars: { CLI_ARGS: 'run {{.NPM_SCRIPT}}' }
- task: sculpin
vars: { CLI_ARGS: 'generate --env="{{.APP_ENV}}" --url="{{.APP_URL}}" --no-interaction -vv' }
make-post:
vars:
POST_DATE:
sh: date +'%Y-%m-%d'
cmd: touch "source/_articles/{{.POST_DATE}}-new-post.md"
deploy:
vars:
COMMIT_HASH:
sh: git rev-parse --short HEAD
TIMESTAMP:
sh: date +%s
DOCKER_IMAGE: homepage-nginx:{{.COMMIT_HASH}}-{{.TIMESTAMP}}
cmds:
- task: build-prod
- docker build --pull --file docker/Dockerfile.nginx.prod --tag {{.DOCKER_IMAGE}} .
- task: deploy-with-ansible
vars:
DOCKER_IMAGE: '{{.DOCKER_IMAGE}}'
deploy-with-ansible:
internal: true
requires:
vars: [DOCKER_IMAGE]
dir: '/home/av/projects/private/pet-project-server'
cmd: ansible-playbook -i production.yml playbook-app-homepage.yml --extra-vars 'homepage_web_image={{.DOCKER_IMAGE}}'

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Homepage\HtmlPrettierBundle\HtmlPrettierBundle;
use Homepage\SiteMapBundle\SiteMapBundle;
use Homepage\TwigExtensionBundle\TwigExtensionBundle;

View File

@ -5,16 +5,6 @@ sculpin:
sculpin_content_types:
posts:
enabled: false
albums:
type: path
path: _albums
singular_name: album
layout: internal
permalink: albums/:basename/
publish_drafts: false
enabled: true
taxonomies:
- tags
articles:
type: path
path: _articles

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\HtmlPrettierBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
@ -9,9 +11,6 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class HtmlPrettierExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));

View File

@ -1,20 +1,17 @@
<?php
declare(strict_types=1);
namespace Homepage\HtmlPrettierBundle;
use Generator;
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
{
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
@ -43,7 +40,7 @@ class HtmlPrettier implements EventSubscriberInterface
}
}
private function filterSource(SourceSet $sourceSet): Generator
private function filterSource(SourceSet $sourceSet): \Generator
{
/** @var SourceInterface $source */
foreach ($sourceSet->allSources() as $source) {
@ -61,7 +58,7 @@ class HtmlPrettier implements EventSubscriberInterface
private function endsWith($haystack, $needle): bool
{
$length = strlen($needle);
$length = \strlen($needle);
return $length === 0 || (substr($haystack, -$length) === $needle);
}

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;
@ -9,9 +11,6 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class SiteMapExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));

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;
@ -21,9 +23,6 @@ class SiteMapGenerator implements DataProviderInterface, EventSubscriberInterfac
*/
private $sources;
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
@ -86,7 +85,7 @@ class SiteMapGenerator implements DataProviderInterface, EventSubscriberInterfac
return [];
}
if ($data['draft']) {
if (array_key_exists('draft', $data) && $data['draft']) {
return [];
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Homepage\TwigExtensionBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
@ -9,9 +11,6 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class TwigExtensionExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));

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": "^8.0",
"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
}
}
}

1974
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

6
docker/node/Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM node:12
ENV npm_config_fund=false
ENV npm_config_update_notifier=false
WORKDIR /srv/app

29
docker/php/Dockerfile Normal file
View File

@ -0,0 +1,29 @@
FROM php:8.1-cli
ENV COMPOSER_FUND=0
RUN apt-get update && apt-get install -y \
curl \
git \
gnupg \
gzip \
libtidy-dev \
rsync \
zip \
;
RUN docker-php-ext-install tidy \
&& docker-php-ext-enable tidy
# Composer and required tools
RUN curl -sLO https://getcomposer.org/download/2.8.4/composer.phar \
&& mv composer.phar /usr/local/bin/composer \
&& chmod +x /usr/local/bin/composer
# PHP-CS-Fixer
RUN curl -sLO https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v3.65.0/php-cs-fixer.phar \
&& mv php-cs-fixer.phar /usr/local/bin/php-cs-fixer \
&& chmod +x /usr/local/bin/php-cs-fixer
WORKDIR /srv/app

View File

@ -1,29 +0,0 @@
#!/usr/bin/env sh
set -eux
apt-get update && apt-get install -y \
curl \
git \
gnupg \
gzip \
libtidy-dev \
rsync \
zip \
;
docker-php-ext-install tidy \
&& docker-php-ext-enable tidy
# Project folder
mkdir -p /srv/app
# Composer and required tools
curl -sLO https://getcomposer.org/download/1.10.8/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

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

@ -1,61 +0,0 @@
---
title: Белое море - 2011, 2013 года
description: Фотографии с поездок на Белое море в самую короткую ночь в году
---
{% block content %}
<p>
<a href="https://img-fotki.yandex.ru/get/9222/46045840.28/0_98765_7fc8895f_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/9222/46045840.28/0_98765_7fc8895f_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/9312/46045840.28/0_98748_e6ab013e_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/9312/46045840.28/0_98748_e6ab013e_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/9231/46045840.28/0_98749_b4fb46a9_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/9231/46045840.28/0_98749_b4fb46a9_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/9305/46045840.28/0_9874a_afbf859b_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/9305/46045840.28/0_9874a_afbf859b_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/9171/46045840.28/0_98747_7d4ca4c9_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/9171/46045840.28/0_98747_7d4ca4c9_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/9217/46045840.28/0_9874b_e42da6b4_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/9217/46045840.28/0_9874b_e42da6b4_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/9228/46045840.28/0_9874f_9af8aa8f_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/9228/46045840.28/0_9874f_9af8aa8f_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/9301/46045840.28/0_9875a_d96bfbd7_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/9301/46045840.28/0_9875a_d96bfbd7_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/9324/46045840.28/0_98756_13fee051_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/9324/46045840.28/0_98756_13fee051_XL.jpg"/>
</a>
</p>
{% endblock %}

View File

@ -1,67 +0,0 @@
---
title: Зимний туман
description: Фотографи набережной Седова, сделанные в туманный зимний день
---
{% block content %}
<p>
<a href="https://img-fotki.yandex.ru/get/5821/46045840.24/0_6f263_3e1f58f6_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/5821/46045840.24/0_6f263_3e1f58f6_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/4419/46045840.24/0_6f25c_397758c6_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/4419/46045840.24/0_6f25c_397758c6_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/5313/46045840.24/0_6f256_e62604eb_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/5313/46045840.24/0_6f256_e62604eb_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/5313/46045840.24/0_6f257_5eaf214c_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/5313/46045840.24/0_6f257_5eaf214c_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/4614/46045840.24/0_6f259_3caa7abe_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/4614/46045840.24/0_6f259_3caa7abe_XXXL.jpg"/>
</a>
</p>
<p>
<a class="photo-link" href="https://img-fotki.yandex.ru/get/4419/46045840.24/0_6f25a_4f1fa9ef_XXXL.jpg">
<img class="photo-image" src="https://img-fotki.yandex.ru/get/4419/46045840.24/0_6f25a_4f1fa9ef_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/4614/46045840.24/0_6f25b_8a14fb24_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/4614/46045840.24/0_6f25b_8a14fb24_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/4419/46045840.24/0_6f25d_b4fd58d3_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/4419/46045840.24/0_6f25d_b4fd58d3_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/4614/46045840.24/0_6f25f_b60733cd_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/4614/46045840.24/0_6f25f_b60733cd_XL.jpg"/>
</a>
</p>
<p>
<a href="https://img-fotki.yandex.ru/get/4418/46045840.24/0_6f255_c458f69c_XXXL.jpg">
<img src="https://img-fotki.yandex.ru/get/4418/46045840.24/0_6f255_c458f69c_XL.jpg"/>
</a>
</p>
{% endblock %}

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()` позволит обнаружить странное поведение.
- Если в пустом поле нет ничего не обычного, то подход "получить и проверить"
будет удобнее, все равно нужно делать проверку.
В любом случае, нужно смотреть на уместность того или иного подхода в каждом
случае, и не использовать их одновременно.

View File

@ -1,78 +0,0 @@
<template>
<section class="app">
<p class="fact-index">Факт {{ factIndex }}</p>
<p class="note">{{ fact }}</p>
<button @click.prevent="next" class="button-next">
Узнать чуть лучше
</button>
</section>
</template>
<script>
import _ from 'underscore';
const NOTES = [
'Люблю фильм "Три идиота".',
'Люблю кататься на велосипеде.',
'Люблю читать фантастические книги.',
'Люблю шоколад.',
'На день рождения ко мне можно прийти без подарка.',
'Не люблю пьяных людей.',
'Предпочитаю ходить в кино на 2D-сеансы.',
'Проехал на велосипеде 200 километров за день.',
'Работаю программистом.',
'Хотел бы побывать в горах.',
];
export default {
data() {
return {
notes: NOTES,
shown: [],
fact: '',
factIndex: null,
};
},
mounted() {
this.pick();
},
methods: {
next() {
this.pick();
},
pick() {
let available = _.difference(this.notes, this.shown);
if (_.size(available) === 0) {
available = this.notes;
this.shown = [];
}
const fact = _.sample(available);
this.shown.push(fact);
this.fact = fact;
this.factIndex = _.indexOf(NOTES, fact) + 1;
},
},
};
</script>
<style lang="scss" scoped>
@import '../components/button';
.app {
text-align: center;
}
.fact-index {
margin-top: 3em;
}
.note {
display: block;
font-size: 160%;
margin: 0 auto 2em;
min-height: 3em;
}
.button-next {
@extend %button;
}
</style>

View File

@ -1,7 +0,0 @@
import Vue from 'vue';
import About from './about.vue';
new Vue({
el: '#app',
render: h => h(About),
});

View File

@ -1,19 +0,0 @@
---
layout: internal
title: Обо мне
description: Несколько фактов об авторе
---
{% block js %}
{{ parent() }}
<script async src="{{ hashed_asset('/static/about.js') }}"></script>
{% endblock %}
{% block css %}
{{ parent() }}
<link rel="stylesheet" href="{{ hashed_asset('/static/about.css') }}">
{% endblock %}
{% block content %}
<div id="app"></div>
{% endblock %}

View File

@ -1,25 +0,0 @@
---
layout: internal
title: Фотоальбомы
description: Фотоальбомы
use: [albums]
---
{% block content %}
<h1>{{ page.title }}</h1>
<ul>
{% for album in data.albums %}
<li>
<p>
<a href="{{ album.url }}">{{ album.title }}</a>
{% if album.description %}
<br>
{{ album.description }}
{% endif %}
</p>
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -25,7 +25,6 @@ use: [articles]
Работаю в <a href="https://playkot.com/" target="_blank">Playkot</a>.
Пишу на PHP.
Разбираюсь в back-end, экспериментирую с front-end, интересуюсь функциональным программированием.
Иногда <a href="/albums/">фотографирую</a>.
</p>
<span class="hr-line"></span>
@ -41,23 +40,8 @@ use: [articles]
</a>
</li>
<li class="social__item">
<a class="social__link" href="https://github.com/anwinged" target="_blank" title="Код на Гитхабе">
<i class="fab fa-github"></i>
</a>
</li>
<li class="social__item">
<a class="social__link" href="https://www.linkedin.com/in/anton-vakhrushev" target="_blank" title="Профиль на Линкедин">
<i class="fab fa-linkedin"></i>
</a>
</li>
<li class="social__item">
<a class="social__link" href="https://instagram.com/anwinged" target="_blank" title="Фотографии в Инстаграме">
<i class="fab fa-instagram"></i>
</a>
</li>
<li class="social__item">
<a class="social__link" href="/about/" target="_blank" title="Об авторе">
<i class="fas fa-ghost"></i>
<a class="social__link" href="https://git.vakhrushev.me" target="_blank" title="Код на Гитхабе">
<i class="fab fa-git-square"></i>
</a>
</li>
<li class="social__item">

View File

@ -1,8 +0,0 @@
#!/bin/bash
source .env
docker build \
--file docker/Dockerfile \
--tag "${PHP_IMAGE}" \
"$PWD"

View File

@ -1,19 +0,0 @@
#!/bin/bash
source .env
mkdir -p var/docker-cache/.composer
docker run \
--rm \
--interactive \
--tty \
--user "$UID:$(id -g)" \
--volume /etc/passwd:/etc/passwd:ro \
--volume /etc/group:/etc/group:ro \
--volume "$PWD:/srv/app" \
--volume "$HOME:$HOME" \
--volume "$PWD/var/docker-cache/.composer:/tmp/.composer" \
--env COMPOSER_HOME=/tmp/.composer \
"${PHP_IMAGE}" \
composer "$@"

View File

@ -1,15 +0,0 @@
#!/bin/bash
source .env
docker run \
--rm \
--interactive \
--tty \
--user "$UID:$(id -g)" \
--volume /etc/passwd:/etc/passwd:ro \
--volume /etc/group:/etc/group:ro \
--volume "$HOME:$HOME" \
--volume "$PWD:/srv/app" \
"${PHP_IMAGE}" \
dep "$@"

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
set -eux
touch "source/_articles/$(date +'%Y-%m-%d')-$1.md"

View File

@ -1,20 +0,0 @@
#!/bin/bash
source .env
mkdir -p var/docker-cache/.npm
docker run \
--rm \
--interactive \
--tty \
--user "$UID:$(id -g)" \
--volume /etc/passwd:/etc/passwd:ro \
--volume /etc/group:/etc/group:ro \
--volume "$PWD:/srv/app" \
--volume "$HOME:$HOME" \
--volume "$PWD/var/docker-cache/.npm:/tmp/.npm" \
--env npm_config_cache=/tmp/.npm \
--workdir /srv/app \
"${NODE_IMAGE}" \
npm "$@"

View File

@ -1,13 +0,0 @@
#!/bin/bash
source .env
docker run \
--rm \
--interactive \
--tty \
--init \
--user "$UID:$(id -g)" \
--volume="$PWD:/srv/app" \
"${PHP_IMAGE}" \
./vendor/bin/php-cs-fixer "$@"

View File

@ -1,15 +0,0 @@
#!/bin/bash
source .env
docker run \
--rm \
--interactive \
--tty \
--init \
--user "$UID:$(id -g)" \
--volume="$PWD:/srv/app" \
--expose=8000 \
--publish=8000:8000 \
"${PHP_IMAGE}" \
./vendor/bin/sculpin "$@"