chore: initial import for test contour
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
.env.*
|
||||
config/secrets/
|
||||
@@ -0,0 +1,17 @@
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{compose.yaml,compose.*.yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,23 @@
|
||||
# In all environments, the following files are loaded if they exist,
|
||||
# the latter taking precedence over the former:
|
||||
#
|
||||
# * .env contains default values for the environment variables needed by the app
|
||||
# * .env.local uncommitted file with local overrides
|
||||
# * .env.$APP_ENV committed environment-specific defaults
|
||||
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||
#
|
||||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
# https://symfony.com/doc/current/configuration/secrets.html
|
||||
#
|
||||
# Run "composer dump-env dev" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_DEBUG=1
|
||||
APP_RUNTIME_ENV=dev
|
||||
APP_RUNTIME_MODE=debug
|
||||
SYMFONY_ENV=dev
|
||||
###< symfony/framework-bundle ###
|
||||
@@ -0,0 +1,3 @@
|
||||
# define your env variables for the test env here
|
||||
KERNEL_CLASS='App\Kernel'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
@@ -0,0 +1,92 @@
|
||||
name: backend-ci-cd
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'backend-v*'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
REGISTRY: git.sova.local
|
||||
IMAGE: git.sova.local/sova/backend
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: pdo_pgsql, redis, intl, zip, gd
|
||||
- run: composer install --prefer-dist --no-interaction
|
||||
- run: composer phpunit || true
|
||||
- run: composer audit || true
|
||||
|
||||
parse-tag:
|
||||
if: startsWith(github.ref, 'refs/tags/backend-v')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
full_tag: ${{ steps.meta.outputs.full_tag }}
|
||||
env: ${{ steps.meta.outputs.env }}
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
steps:
|
||||
- name: Parse tag
|
||||
id: meta
|
||||
run: |
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "full_tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "env=$(echo "$TAG" | sed -E 's/backend-v([0-9.]+)-([a-z]+)/\2/')" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$(echo "$TAG" | sed -E 's/backend-v([0-9.]+).*/\1/')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build-and-push:
|
||||
needs: [test, parse-tag]
|
||||
if: startsWith(github.ref, 'refs/tags/backend-v')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Docker login
|
||||
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "$REGISTRY" -u sova-ci --password-stdin
|
||||
- name: Build and push
|
||||
run: |
|
||||
TAG="${{ needs.parse-tag.outputs.full_tag }}"
|
||||
docker build -f Dockerfile -t "$IMAGE:${TAG}" -t "$IMAGE:${{ needs.parse-tag.outputs.version }}" .
|
||||
docker push "$IMAGE:${TAG}"
|
||||
docker push "$IMAGE:${{ needs.parse-tag.outputs.version }}"
|
||||
|
||||
deploy-gitops:
|
||||
needs: [build-and-push, parse-tag]
|
||||
if: startsWith(github.ref, 'refs/tags/backend-v')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Bump image tag in sova-deploy
|
||||
env:
|
||||
DEPLOY_KEY: ${{ secrets.SOVA_DEPLOY_KEY }}
|
||||
run: |
|
||||
eval "$(ssh-agent -s)"
|
||||
echo "$DEPLOY_KEY" | ssh-add -
|
||||
git clone git@gitea.sova.local:sova/sova-deploy.git
|
||||
cd sova-deploy
|
||||
ENV="${{ needs.parse-tag.outputs.env }}"
|
||||
TAG="${{ needs.parse-tag.outputs.full_tag }}"
|
||||
git config user.email "ci-bot@sova.local"
|
||||
git config user.name "sova-ci"
|
||||
MAX_RETRIES=5
|
||||
for attempt in $(seq 1 $MAX_RETRIES); do
|
||||
git pull --rebase origin main
|
||||
yq -i ".image.tag = \"${TAG}\"" "apps/backend/values-${ENV}.yaml"
|
||||
git add "apps/backend/values-${ENV}.yaml"
|
||||
git diff --cached --quiet && { echo "No changes"; exit 0; }
|
||||
git commit -m "chore(backend): bump ${ENV} to ${TAG}"
|
||||
if git push origin main; then
|
||||
echo "Push OK on attempt ${attempt}"
|
||||
exit 0
|
||||
fi
|
||||
echo "Push failed, retry ${attempt}/${MAX_RETRIES}..."
|
||||
git reset --hard HEAD~1
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
echo "Failed to push after ${MAX_RETRIES} attempts"
|
||||
exit 1
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/public/uploads/
|
||||
/var/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
composer.lock
|
||||
symfony.lock
|
||||
yarn.lock
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
/config/jwt/*.pem
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
/.phpunit.cache/
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> symfony/asset-mapper ###
|
||||
/public/assets/
|
||||
/assets/vendor/
|
||||
###< symfony/asset-mapper ###
|
||||
|
||||
/php:
|
||||
|
||||
.cursorignore
|
||||
.env
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM composer:2 AS vendor
|
||||
WORKDIR /app
|
||||
COPY composer.json composer.lock symfony.lock ./
|
||||
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction --ignore-platform-reqs
|
||||
COPY . .
|
||||
RUN composer dump-autoload --classmap-authoritative --no-dev \
|
||||
&& composer run-script --no-dev post-install-cmd || true
|
||||
|
||||
FROM php:8.4-fpm-alpine AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache \
|
||||
tzdata postgresql-dev postgresql-client libpq libzip-dev \
|
||||
libjpeg-turbo-dev freetype-dev libwebp-dev libpng-dev icu-dev \
|
||||
oniguruma-dev bash autoconf g++ make \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
zip pdo pdo_pgsql pdo_mysql gd intl opcache \
|
||||
&& pecl install redis \
|
||||
&& docker-php-ext-enable redis \
|
||||
&& rm -rf /tmp/pear /var/cache/apk/*
|
||||
|
||||
ENV TZ=Europe/Moscow
|
||||
RUN cp /usr/share/zoneinfo/$TZ /etc/localtime && echo "$TZ" > /etc/timezone
|
||||
|
||||
COPY docker/fpm-pool.conf /usr/local/etc/php-fpm.d/zz-docker.conf
|
||||
COPY --from=vendor /app /app
|
||||
|
||||
RUN mkdir -p var/cache var/log public/uploads \
|
||||
&& chown -R www-data:www-data var public/uploads \
|
||||
&& chmod -R 775 var
|
||||
|
||||
ENV APP_ENV=prod APP_DEBUG=0
|
||||
RUN APP_SECRET=build-placeholder \
|
||||
DATABASE_URL="postgresql://build:build@127.0.0.1:5432/build" \
|
||||
DATABASE_CABINET_URL="postgresql://build:build@127.0.0.1:5432/build" \
|
||||
DATABASE_BITRIX_URL="mysql://build:build@127.0.0.1:3306/build" \
|
||||
REDIS_URL="redis://127.0.0.1:6379" \
|
||||
php bin/console cache:warmup --env=prod || true
|
||||
|
||||
USER www-data
|
||||
EXPOSE 9000
|
||||
CMD ["php-fpm", "-F"]
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
import './bootstrap.js';
|
||||
/*
|
||||
* Welcome to your app's main JavaScript file!
|
||||
*
|
||||
* This file will be included onto the page via the importmap() Twig function,
|
||||
* which should already be in your base.html.twig.
|
||||
*/
|
||||
import './styles/app.css';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||
|
||||
const app = startStimulusApp();
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
|
||||
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
|
||||
|
||||
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
|
||||
document.addEventListener('submit', function (event) {
|
||||
generateCsrfToken(event.target);
|
||||
}, true);
|
||||
|
||||
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
|
||||
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
|
||||
document.addEventListener('turbo:submit-start', function (event) {
|
||||
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
|
||||
Object.keys(h).map(function (k) {
|
||||
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
|
||||
});
|
||||
});
|
||||
|
||||
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
|
||||
document.addEventListener('turbo:submit-end', function (event) {
|
||||
removeCsrfToken(event.detail.formSubmission.formElement);
|
||||
});
|
||||
|
||||
export function generateCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
let csrfToken = csrfField.value;
|
||||
|
||||
if (!csrfCookie && nameCheck.test(csrfToken)) {
|
||||
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
|
||||
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
|
||||
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
if (csrfCookie && tokenCheck.test(csrfToken)) {
|
||||
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
export function generateCsrfHeaders (formElement) {
|
||||
const headers = {};
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
headers[csrfCookie] = csrfField.value;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function removeCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
|
||||
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default 'csrf-protection-controller';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* This is an example Stimulus controller!
|
||||
*
|
||||
* Any element with a data-controller="hello" attribute will cause
|
||||
* this controller to be executed. The name "hello" comes from the filename:
|
||||
* hello_controller.js -> "hello"
|
||||
*
|
||||
* Delete this file or adapt it for your use!
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||
|
||||
const app = startStimulusApp();
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
||||
@@ -0,0 +1,54 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: 'Ubuntu', sans-serif;
|
||||
height: 100vh;
|
||||
min-height: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #344A5E;
|
||||
}
|
||||
.central-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.bottom-content {
|
||||
height: 116px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.caption {
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.big-text {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
}
|
||||
.small-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
.ref {
|
||||
font-size: 14px;
|
||||
color: #0279C0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ref:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.pic {
|
||||
margin-left: 45px;
|
||||
margin-bottom: 15px;
|
||||
margin-top: -70px;
|
||||
}
|
||||
.b-text_lang_ru {
|
||||
display: none;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
|
||||
return new Application($kernel);
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (!ini_get('date.timezone')) {
|
||||
ini_set('date.timezone', 'UTC');
|
||||
}
|
||||
|
||||
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
|
||||
if (PHP_VERSION_ID >= 80000) {
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
} else {
|
||||
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
|
||||
require PHPUNIT_COMPOSER_INSTALL;
|
||||
PHPUnit\TextUI\Command::main();
|
||||
}
|
||||
} else {
|
||||
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
services:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
ports:
|
||||
- "5432"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
mailer:
|
||||
image: axllent/mailpit
|
||||
ports:
|
||||
- "1025"
|
||||
- "8025"
|
||||
environment:
|
||||
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
||||
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
||||
###< symfony/mailer ###
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
services:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
image: postgres:${POSTGRES_VERSION:-16}-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
# You should definitely change the password in production
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
volumes:
|
||||
- database_data:/var/lib/postgresql/data:rw
|
||||
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
|
||||
# - ./docker/db/data:/var/lib/postgresql/data:rw
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
volumes:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database_data:
|
||||
###< doctrine/doctrine-bundle ###
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"name": "sova/exo",
|
||||
"description": "Единое хранилище данных ",
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-iconv": "*",
|
||||
"babdev/pagerfanta-bundle": "^4.5",
|
||||
"doctrine/dbal": "^4.3.2",
|
||||
"doctrine/doctrine-bundle": "^2.15.1",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.4.2",
|
||||
"doctrine/orm": "^3.5.2",
|
||||
"dragonmantank/cron-expression": "^3.4",
|
||||
"lexik/jwt-authentication-bundle": "^3.1.1",
|
||||
"nelmio/api-doc-bundle": "^5.5",
|
||||
"nelmio/cors-bundle": "^2.5",
|
||||
"pagerfanta/doctrine-orm-adapter": "^4.7.1",
|
||||
"phpdocumentor/reflection-docblock": "^5.6.2",
|
||||
"phpstan/phpdoc-parser": "^2.2",
|
||||
"predis/predis": "^3.2",
|
||||
"scienta/doctrine-json-functions": "^6.3",
|
||||
"symfony/asset": "^7.3.0",
|
||||
"symfony/asset-mapper": "^7.3.2",
|
||||
"symfony/cache": "^7.3.2",
|
||||
"symfony/config": "^7.3.2",
|
||||
"symfony/console": "^7.3.2",
|
||||
"symfony/doctrine-messenger": "^7.3.2",
|
||||
"symfony/dotenv": "^7.3.2",
|
||||
"symfony/event-dispatcher": "^7.3.0",
|
||||
"symfony/expression-language": "^7.3.2",
|
||||
"symfony/flex": "^2.8.1",
|
||||
"symfony/form": "^7.3.2",
|
||||
"symfony/framework-bundle": "^7.3.2",
|
||||
"symfony/http-client": "^7.3.2",
|
||||
"symfony/intl": "^7.3.2",
|
||||
"symfony/lock": "^7.3.2",
|
||||
"symfony/mailer": "^7.3.2",
|
||||
"symfony/messenger": "^7.3.2",
|
||||
"symfony/mime": "^7.3.2",
|
||||
"symfony/monolog-bundle": "^3.10",
|
||||
"symfony/notifier": "^7.3.0",
|
||||
"symfony/password-hasher": "^7.3.0",
|
||||
"symfony/process": "^7.3.0",
|
||||
"symfony/property-access": "^7.3.2",
|
||||
"symfony/property-info": "^7.3.1",
|
||||
"symfony/rate-limiter": "^7.3.2",
|
||||
"symfony/runtime": "^7.3.1",
|
||||
"symfony/scheduler": "^7.3.2",
|
||||
"symfony/security-bundle": "^7.3.2",
|
||||
"symfony/serializer": "^7.3.2",
|
||||
"symfony/stimulus-bundle": "^2.29.1",
|
||||
"symfony/string": "^7.3.2",
|
||||
"symfony/translation": "^7.3.2",
|
||||
"symfony/twig-bundle": "^7.3.2",
|
||||
"symfony/ux-turbo": "^2.29.1",
|
||||
"symfony/validator": "^7.3.2",
|
||||
"symfony/web-link": "^7.3.0",
|
||||
"symfony/yaml": "^7.3.2",
|
||||
"twig/extra-bundle": "^2.12|^3.21",
|
||||
"twig/twig": "^2.12|^3.21.1"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
},
|
||||
"bump-after-update": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php72": "*",
|
||||
"symfony/polyfill-php73": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php82": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"cache:clear-all": [
|
||||
"@php bin/console doctrine:cache:clear-metadata",
|
||||
"@php bin/console doctrine:cache:clear-query",
|
||||
"@php bin/console doctrine:cache:clear-result",
|
||||
"@php bin/console cache:clear"
|
||||
],
|
||||
"phpunit": "phpunit",
|
||||
"generate-swagger": "php ./vendor/bin/openapi --output ./public/swagger.json ./src",
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd",
|
||||
"importmap:install": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
]
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": "false",
|
||||
"require": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpspec/prophecy": "^1.22",
|
||||
"phpunit/phpunit": "^12.3.5",
|
||||
"symfony/browser-kit": "^7.3.2",
|
||||
"symfony/css-selector": "^7.3.0",
|
||||
"symfony/debug-bundle": "^7.3.0",
|
||||
"symfony/maker-bundle": "^1.64",
|
||||
"symfony/stopwatch": "^7.3.0",
|
||||
"symfony/web-profiler-bundle": "^7.3.0"
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
BabDev\PagerfantaBundle\BabDevPagerfantaBundle::class => ['all' => true],
|
||||
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
framework:
|
||||
asset_mapper:
|
||||
# The paths to make available to the asset mapper.
|
||||
paths:
|
||||
- assets/
|
||||
missing_import_mode: strict
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
asset_mapper:
|
||||
missing_import_mode: warn
|
||||
@@ -0,0 +1,10 @@
|
||||
framework:
|
||||
cache:
|
||||
app: cache.adapter.redis
|
||||
default_redis_provider: '%env(resolve:REDIS_URL)%'
|
||||
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.app
|
||||
@@ -0,0 +1,11 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
form:
|
||||
csrf_protection:
|
||||
token_id: submit
|
||||
|
||||
csrf_protection:
|
||||
stateless_token_ids:
|
||||
- submit
|
||||
- authenticate
|
||||
- logout
|
||||
@@ -0,0 +1,5 @@
|
||||
when@dev:
|
||||
debug:
|
||||
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||
# See the "server:dump" command to start a new server.
|
||||
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
||||
@@ -0,0 +1,14 @@
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
collect: true
|
||||
only_exceptions: false
|
||||
collect_serializer_data: true
|
||||
|
||||
# Уберите опцию profiling, она больше не существует
|
||||
http_client:
|
||||
# Профилирование теперь включается автоматически в dev среде
|
||||
# при наличии установленного web_profiler
|
||||
@@ -0,0 +1,92 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
connections:
|
||||
default: # PostgreSQL
|
||||
schema_filter: ~^(?!cron)~
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
logging: true
|
||||
profiling: true
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
use_savepoints: true
|
||||
|
||||
mysql: # Bitrix MySQL
|
||||
url: '%env(resolve:DATABASE_BITRIX_URL)%'
|
||||
driver: pdo_mysql
|
||||
logging: true
|
||||
profiling: true
|
||||
|
||||
cabinet: # Cabinet PostgreSQL
|
||||
url: '%env(resolve:DATABASE_CABINET_URL)%'
|
||||
logging: true
|
||||
profiling: true
|
||||
orm:
|
||||
dql:
|
||||
string_functions:
|
||||
JSONB_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonbContains
|
||||
JSON_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonContains
|
||||
JSONB_EXISTS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonbExists
|
||||
JSONB_EXISTS_ANY: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonbExistsAny
|
||||
JSONB_EXISTS_ALL: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonbExistsAll
|
||||
auto_generate_proxy_classes: false
|
||||
metadata_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
enable_lazy_ghost_objects: true
|
||||
report_fields_where_declared: true
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
identity_generation_preferences:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
when@dev:
|
||||
doctrine:
|
||||
orm:
|
||||
# In dev, avoid Redis-backed metadata/query cache: stale ClassMetadata (e.g. removed fields) breaks warmup.
|
||||
metadata_cache_driver:
|
||||
type: pool
|
||||
pool: cache.system
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: cache.system
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
dbal:
|
||||
# "TEST_TOKEN" is typically set by ParaTest
|
||||
# dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
auto_generate_proxy_classes: false
|
||||
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
@@ -0,0 +1,6 @@
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
||||
@@ -0,0 +1,31 @@
|
||||
framework:
|
||||
http_method_override: false
|
||||
handle_all_throwables: true
|
||||
secret: '%env(APP_SECRET)%'
|
||||
|
||||
session:
|
||||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
http_client:
|
||||
default_options:
|
||||
max_duration: 30
|
||||
|
||||
#esi: false
|
||||
#fragments: false
|
||||
|
||||
when@dev:
|
||||
framework:
|
||||
php_errors:
|
||||
throw: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
@@ -0,0 +1,5 @@
|
||||
lexik_jwt_authentication:
|
||||
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
|
||||
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
||||
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||
token_ttl: 31536000
|
||||
@@ -0,0 +1,2 @@
|
||||
framework:
|
||||
lock: '%env(LOCK_DSN)%'
|
||||
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
mailer:
|
||||
dsn: '%env(MAILER_DSN)%'
|
||||
@@ -0,0 +1,24 @@
|
||||
framework:
|
||||
messenger:
|
||||
enabled: true
|
||||
failure_transport: failed
|
||||
transports:
|
||||
sync: 'sync://'
|
||||
failed: 'doctrine://default?queue_name=failed'
|
||||
scheduler_default:
|
||||
dsn: '%env(resolve:MESSENGER_TRANSPORT_DSN)%'
|
||||
options:
|
||||
queue_name: scheduler_default
|
||||
routing:
|
||||
Symfony\Component\Scheduler\Messenger\SchedulerTransport: scheduler_default
|
||||
App\Message\GetScheduleMessage: sync
|
||||
App\Message\GetSpecialistPictureMessage: sync
|
||||
App\Message\GetAnonymousReserveRequestMessage: sync
|
||||
|
||||
# when@test:
|
||||
# framework:
|
||||
# messenger:
|
||||
# transports:
|
||||
# # replace with your transport name here (e.g., my_transport: 'in-memory://')
|
||||
# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test
|
||||
# async: 'in-memory://'
|
||||
@@ -0,0 +1,76 @@
|
||||
monolog:
|
||||
channels:
|
||||
- infoclinica
|
||||
- deprecation
|
||||
- bitrix
|
||||
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event", "!http_client"]
|
||||
http_client:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/http_client.log"
|
||||
level: debug
|
||||
channels: ["http_client"]
|
||||
messenger:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/messenger.log"
|
||||
level: debug
|
||||
channels: ["messenger"]
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
infoclinica:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/infoclinica-%kernel.environment%.log"
|
||||
formatter: monolog.formatter.json
|
||||
channels: ["infoclinica"]
|
||||
bitrix:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/bitrix-%kernel.environment%.log"
|
||||
formatter: monolog.formatter.json
|
||||
channels: ["bitrix"]
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event", "!doctrine"]
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
infoclinica:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/infoclinica-%kernel.environment%.log"
|
||||
formatter: monolog.formatter.json
|
||||
channels: ["infoclinica"]
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50
|
||||
formatter: monolog.formatter.json
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
console:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/console-%kernel.environment%.log"
|
||||
max_files: 7
|
||||
level: debug
|
||||
channels: ["!event", "!doctrine"]
|
||||
formatter: monolog.formatter.json
|
||||
@@ -0,0 +1,26 @@
|
||||
nelmio_api_doc:
|
||||
documentation:
|
||||
servers:
|
||||
- url: https://api.sovamed.ru/
|
||||
description: Public API - sovamed
|
||||
- url: https://api.wmtmed.ru/
|
||||
description: Public API - wmtmed
|
||||
info:
|
||||
title: Public API
|
||||
description: Справочник методов доступных в Public API
|
||||
version: 1.0.0
|
||||
areas:
|
||||
path_patterns: [
|
||||
'^/filial/list$',
|
||||
'^/department/list$',
|
||||
'^/specialist/list$',
|
||||
'^/specialist/schedule$',
|
||||
'^/pricelist/list$',
|
||||
'^/pricelist/department$',
|
||||
'^/news($|/)',
|
||||
'^/promo($|/)',
|
||||
'^/disease($|/)',
|
||||
'^/medical-center($|/)',
|
||||
'^/article($|/)',
|
||||
'^/site-services($|/)'
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
nelmio_cors:
|
||||
defaults:
|
||||
origin_regex: true
|
||||
allow_credentials: true
|
||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
expose_headers: ['Link']
|
||||
max_age: 3600
|
||||
skip_same_as_origin: true
|
||||
paths:
|
||||
'^/': ~
|
||||
@@ -0,0 +1,12 @@
|
||||
framework:
|
||||
notifier:
|
||||
chatter_transports:
|
||||
texter_transports:
|
||||
channel_policy:
|
||||
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
|
||||
urgent: ['email']
|
||||
high: ['email']
|
||||
medium: ['email']
|
||||
low: ['email']
|
||||
admin_recipients:
|
||||
- { email: admin@example.com }
|
||||
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
@@ -0,0 +1,11 @@
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: false
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
#default_uri: http://localhost
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
||||
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
scheduler:
|
||||
enabled: false
|
||||
@@ -0,0 +1,38 @@
|
||||
security:
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
|
||||
providers:
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: email
|
||||
|
||||
firewalls:
|
||||
api:
|
||||
pattern: ^/
|
||||
stateless: true
|
||||
provider: app_user_provider
|
||||
jwt: ~
|
||||
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
login_throttling:
|
||||
max_attempts: 3
|
||||
interval: '15 minutes'
|
||||
logout:
|
||||
path: user_logout
|
||||
|
||||
access_control:
|
||||
# - { path: ^/api/auth, roles: PUBLIC_ACCESS }
|
||||
# - { path: ^/api, roles: ROLE_USER }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4
|
||||
time_cost: 3
|
||||
memory_cost: 10
|
||||
@@ -0,0 +1,5 @@
|
||||
framework:
|
||||
serializer:
|
||||
enabled: true
|
||||
default_context:
|
||||
date_format: 'Y-m-d'
|
||||
@@ -0,0 +1,5 @@
|
||||
framework:
|
||||
default_locale: ru
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
providers:
|
||||
@@ -0,0 +1,6 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
@@ -0,0 +1,4 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
csrf_protection:
|
||||
check_header: true
|
||||
@@ -0,0 +1,12 @@
|
||||
framework:
|
||||
validation:
|
||||
email_validation_mode: html5
|
||||
# Enables validator auto-mapping support.
|
||||
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||
#auto_mapping:
|
||||
# App\Entity\: []
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
||||
@@ -0,0 +1,13 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
profiler:
|
||||
collect: false
|
||||
collect_serializer_data: true
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
controllers:
|
||||
resource:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
@@ -0,0 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
||||
@@ -0,0 +1,12 @@
|
||||
# Expose your documentation as JSON swagger compliant
|
||||
app.swagger:
|
||||
path: /api/doc.json
|
||||
methods: GET
|
||||
defaults: { _controller: nelmio_api_doc.controller.swagger }
|
||||
|
||||
## Requires the Asset component and the Twig bundle
|
||||
## $ composer require twig asset
|
||||
app.swagger_ui:
|
||||
path: docs
|
||||
methods: GET
|
||||
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
|
||||
@@ -0,0 +1,3 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
||||
@@ -0,0 +1,8 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
|
||||
prefix: /_profiler
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $container): void {
|
||||
$stubMode = $_ENV['INTEGRATIONS_STUB_MODE'] ?? $_SERVER['INTEGRATIONS_STUB_MODE'] ?? 'false';
|
||||
$appEnv = $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'prod';
|
||||
|
||||
if (filter_var($stubMode, FILTER_VALIDATE_BOOL) || in_array($appEnv, ['dev', 'test'], true)) {
|
||||
$container->import('./services_stub.yaml');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,232 @@
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
app.timezone: 'Europe/Moscow'
|
||||
upload_directory: '%kernel.project_dir%/public/uploads'
|
||||
api.baseurl: '%env(string:API_BASE_URL)%'
|
||||
api.public_url: '%env(default:api_base_url_default:API_PUBLIC_URL)%'
|
||||
api_base_url_default: '%env(API_BASE_URL)%'
|
||||
env(WIDGET_API_URL): ''
|
||||
widget_api_url: '%env(default:mis_url_default:WIDGET_API_URL)%'
|
||||
mis_url_default: '%env(MIS_URL)%'
|
||||
mailer_from_email: 'noreply@sova.clinic'
|
||||
mailer_from_name: 'Sova Clinic'
|
||||
mailer_access_token: ''
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
Psr\Log\LoggerInterface: '@logger'
|
||||
|
||||
App\MessageHandler\SchedulerDefaultMessageHandler:
|
||||
tags: ['monolog.logger']
|
||||
# arguments:
|
||||
# $application: '@console.messenger.application'
|
||||
|
||||
App\Serializer\Normalizer\SpecialistNormalizer:
|
||||
public: true
|
||||
tags: [serializer.normalizer]
|
||||
|
||||
App\Serializer\Normalizer\StockNormalizer:
|
||||
public: true
|
||||
tags: [serializer.normalizer]
|
||||
|
||||
App\Serializer\Normalizer\SpecialistDocsNormalizer:
|
||||
public: true
|
||||
tags: [serializer.normalizer]
|
||||
|
||||
App\Service\FileUploader\FileUploaderService:
|
||||
tags: ['monolog.logger']
|
||||
public: true
|
||||
arguments:
|
||||
$targetDirectory: '%upload_directory%'
|
||||
|
||||
App\EventListener\JsonExceptionHandler:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
|
||||
|
||||
App\Service\Translite\Interfaces\TransliteServiceInterface:
|
||||
alias: App\Service\Translite\TransliteService
|
||||
|
||||
App\Command\UploadFilialsCommand:
|
||||
arguments:
|
||||
$widgetApiUrl: '%widget_api_url%'
|
||||
tags: ['console.command']
|
||||
|
||||
App\Command\UploadDoctorsCommand:
|
||||
tags: ['console.command']
|
||||
|
||||
App\Command\UploadDepartmentsCommand:
|
||||
tags: ['console.command']
|
||||
|
||||
App\Command\UploadPriceDepCommand:
|
||||
arguments:
|
||||
$widgetApiUrl: '%widget_api_url%'
|
||||
tags: ['console.command']
|
||||
|
||||
App\Command\UploadPriceCommand:
|
||||
arguments:
|
||||
$widgetApiUrl: '%widget_api_url%'
|
||||
tags: ['console.command']
|
||||
|
||||
App\Command\BitrixUpdateDoctorsCommand:
|
||||
arguments:
|
||||
$logger: '@logger'
|
||||
$entityManager: '@doctrine.orm.entity_manager'
|
||||
$bitrixService: '@App\Service\Bitrix\BitrixService'
|
||||
tags: ['console.command']
|
||||
|
||||
App\Command\BitrixUpdateReviewsCommand:
|
||||
arguments:
|
||||
$logger: '@logger'
|
||||
$entityManager: '@doctrine.orm.entity_manager'
|
||||
$bitrixService: '@App\Service\Bitrix\BitrixService'
|
||||
tags: ['console.command']
|
||||
|
||||
App\Service\Crypt\AESCryptService:
|
||||
arguments:
|
||||
$secretKey: '%env(string:AES_SECRET_KEY)%'
|
||||
$cipher: '%env(string:AES_CIPHER_METHOD)%'
|
||||
|
||||
App\Service\Client\AbstractHttpClientService:
|
||||
abstract: true
|
||||
public: false
|
||||
arguments:
|
||||
$userAgent: '%env(string:API_CLIENT)%'
|
||||
$baseUrl: '@api.baseurl'
|
||||
|
||||
App\Service\Client\CalltouchClientService:
|
||||
parent: App\Service\Client\AbstractHttpClientService
|
||||
arguments:
|
||||
$userAgent: '%env(string:API_CLIENT)%'
|
||||
$baseUrl: '%env(string:CT_URL)%'
|
||||
$params: '%env(string:CT_PARAMS)%'
|
||||
|
||||
App\Service\Client\BitrixClientService:
|
||||
parent: App\Service\Client\AbstractHttpClientService
|
||||
arguments:
|
||||
$userAgent: '%env(string:API_CLIENT)%'
|
||||
$baseUrl: '%env(string:BITRIX_URL)%'
|
||||
|
||||
App\Service\Client\InfoclinicaClientService:
|
||||
parent: App\Service\Client\AbstractHttpClientService
|
||||
arguments:
|
||||
$userAgent: '%env(string:API_CLIENT)%'
|
||||
$baseUrl: '%env(string:MIS_URL)%'
|
||||
|
||||
App\Service\Client\SmartCaptchaClientService:
|
||||
parent: App\Service\Client\AbstractHttpClientService
|
||||
arguments:
|
||||
$userAgent: '%env(string:API_CLIENT)%'
|
||||
$baseUrl: '%env(string:SMARTCAPTCHA_URL)%'
|
||||
$secret: '%env(string:SMARTCAPTCHA_KEY)%'
|
||||
|
||||
App\Service\Client\Sms4bClientService:
|
||||
parent: App\Service\Client\AbstractHttpClientService
|
||||
arguments:
|
||||
$userAgent: '%env(string:API_CLIENT)%'
|
||||
$baseUrl: '%env(string:SMS4B_URL)%'
|
||||
$token: '%env(string:SMS4B_TOKEN)%'
|
||||
$sender: '%env(string:SMS4B_SENDER)%'
|
||||
|
||||
App\Service\Client\SmsruClientService:
|
||||
parent: App\Service\Client\AbstractHttpClientService
|
||||
arguments:
|
||||
$userAgent: '%env(string:API_CLIENT)%'
|
||||
$baseUrl: '%env(string:SMSRU_URL)%'
|
||||
$token: '%env(string:SMSRU_TOKEN)%'
|
||||
$sender: '%env(string:SMSRU_SENDER)%'
|
||||
|
||||
App\Service\Client\Interfaces\CalltouchClientServiceInterface:
|
||||
alias: App\Service\Client\CalltouchClientService
|
||||
|
||||
App\Service\Client\Interfaces\SmartCaptchaClientServiceInterface:
|
||||
alias: App\Service\Client\SmartCaptchaClientService
|
||||
|
||||
App\Service\Client\Interfaces\SmsClientServiceInterface:
|
||||
alias: App\Service\Client\SmsruClientService
|
||||
|
||||
App\Service\Bitrix\BitrixService:
|
||||
public: true
|
||||
arguments:
|
||||
$connection: '@doctrine.dbal.mysql_connection'
|
||||
|
||||
App\Service\PriceList\PriceListService:
|
||||
arguments:
|
||||
$priceListRepository: '@App\Repository\PriceListRepository'
|
||||
|
||||
App\Service\Location\LocationService:
|
||||
arguments:
|
||||
$locationRepository: '@App\Repository\LocationRepository'
|
||||
|
||||
App\Service\Specialist\SpecialistService:
|
||||
arguments:
|
||||
$messageBus: '@messenger.default_bus'
|
||||
$specialistRepository: '@App\Repository\SpecialistRepository'
|
||||
|
||||
App\Service\Filial\FilialService:
|
||||
arguments:
|
||||
$filialRepository: '@App\Repository\FilialRepository'
|
||||
|
||||
App\Service\XmlFeedGenerator\XmlFeedGeneratorService:
|
||||
arguments:
|
||||
$priceListService: '@App\Service\PriceList\PriceListService'
|
||||
$departmentService: '@App\Service\Department\DepartmentService'
|
||||
$specialistService: '@App\Service\Specialist\SpecialistService'
|
||||
$locationService: '@App\Service\Location\LocationService'
|
||||
$filialService: '@App\Service\Filial\FilialService'
|
||||
$apiPublicUrl: '%api.public_url%'
|
||||
|
||||
App\Service\XmlFeedGenerator\XmlFeedGeneratorV1Service:
|
||||
arguments:
|
||||
$priceListService: '@App\Service\PriceList\PriceListService'
|
||||
$specialistService: '@App\Service\Specialist\SpecialistService'
|
||||
$helperService: '@App\Service\Helper\HelperService'
|
||||
$connection: '@doctrine.dbal.default_connection'
|
||||
$logger: '@logger'
|
||||
$apiPublicUrl: '%api.public_url%'
|
||||
|
||||
App\Service\ScheduleCache\ScheduleCacheService:
|
||||
arguments:
|
||||
$logger: '@logger'
|
||||
|
||||
App\Service\ErrorHandler\ScheduleErrorHandlerService:
|
||||
arguments:
|
||||
$logger: '@logger'
|
||||
|
||||
App\Service\Performance\PerformanceTrackerService: ~
|
||||
|
||||
App\Service\Mail\SendMailService:
|
||||
arguments:
|
||||
$fromEmail: '%mailer_from_email%'
|
||||
$fromName: '%mailer_from_name%'
|
||||
|
||||
App\Service\Mail\SendMailConfig:
|
||||
arguments:
|
||||
$accessToken: '%env(default:mailer_access_token:MAILER_ACCESS_TOKEN)%'
|
||||
|
||||
App\MessageHandler\GetScheduleMessageHandler:
|
||||
arguments:
|
||||
$clientService: '@App\Service\Client\InfoclinicaClientService'
|
||||
$cacheService: '@App\Service\ScheduleCache\ScheduleCacheService'
|
||||
$errorHandler: '@App\Service\ErrorHandler\ScheduleErrorHandlerService'
|
||||
$performanceTracker: '@App\Service\Performance\PerformanceTrackerService'
|
||||
tags:
|
||||
- { name: messenger.message_handler }
|
||||
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
App\Service\Client\Stub\NoopSmsClientService: ~
|
||||
App\Service\Client\Stub\NoopCalltouchClientService: ~
|
||||
App\Service\Client\Stub\AlwaysValidSmartCaptchaClientService: ~
|
||||
|
||||
App\Service\Client\Interfaces\SmsClientServiceInterface:
|
||||
alias: App\Service\Client\Stub\NoopSmsClientService
|
||||
|
||||
App\Service\Client\Interfaces\CalltouchClientServiceInterface:
|
||||
alias: App\Service\Client\Stub\NoopCalltouchClientService
|
||||
|
||||
App\Service\Client\Interfaces\SmartCaptchaClientServiceInterface:
|
||||
alias: App\Service\Client\Stub\AlwaysValidSmartCaptchaClientService
|
||||
@@ -0,0 +1,11 @@
|
||||
[www]
|
||||
user = www-data
|
||||
group = www-data
|
||||
listen = 127.0.0.1:9000
|
||||
pm = dynamic
|
||||
pm.max_children = 20
|
||||
pm.start_servers = 4
|
||||
pm.min_spare_servers = 2
|
||||
pm.max_spare_servers = 8
|
||||
clear_env = no
|
||||
catch_workers_output = yes
|
||||
@@ -0,0 +1,32 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /app/public;
|
||||
index index.php;
|
||||
client_max_body_size 108M;
|
||||
|
||||
location / {
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~* \.(?:jpg|jpeg|gif|png|ico|css|js|svg|woff2)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~ ^/index\.php(/|$) {
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||
fastcgi_param DOCUMENT_ROOT $realpath_root;
|
||||
fastcgi_param HTTP_PROXY "";
|
||||
internal;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Returns the importmap for this application.
|
||||
*
|
||||
* - "path" is a path inside the asset mapper system. Use the
|
||||
* "debug:asset-map" command to see the full list of paths.
|
||||
*
|
||||
* - "entrypoint" (JavaScript only) set to true for any module that will
|
||||
* be used as an "entrypoint" (and passed to the importmap() Twig function).
|
||||
*
|
||||
* The "importmap:require" command can be used to add new entries to this file.
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'path' => './assets/app.js',
|
||||
'entrypoint' => true,
|
||||
],
|
||||
'@hotwired/stimulus' => [
|
||||
'version' => '3.2.2',
|
||||
],
|
||||
'@symfony/stimulus-bundle' => [
|
||||
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
|
||||
],
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '7.3.0',
|
||||
],
|
||||
];
|
||||
+59347
File diff suppressed because one or more lines are too long
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260213132749 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create article table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE article (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||
name TEXT,
|
||||
preview_picture TEXT,
|
||||
active BOOLEAN DEFAULT NULL,
|
||||
doctors JSONB DEFAULT NULL,
|
||||
services JSONB DEFAULT NULL,
|
||||
region_id INT DEFAULT NULL,
|
||||
alias TEXT,
|
||||
anons TEXT,
|
||||
content TEXT,
|
||||
update_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)');
|
||||
$this->addSql('CREATE INDEX idx_article_active ON article (active)');
|
||||
$this->addSql('CREATE INDEX idx_article_region_id ON article (region_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE article');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260213132759 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create view_article and populate article table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Создаем представление view_article, если его еще нет
|
||||
$this->addSql('CREATE OR REPLACE VIEW public.view_article
|
||||
AS WITH article_data AS (
|
||||
SELECT el.id AS article_id,
|
||||
TRIM(TRAILING \'-\'::text FROM el.code) AS group_code,
|
||||
el.id,
|
||||
el.name,
|
||||
el.preview_picture,
|
||||
f.subdir,
|
||||
f.file_name,
|
||||
el.active,
|
||||
el.iblock_id,
|
||||
el.iblock_section_id,
|
||||
el.code,
|
||||
el.preview_text,
|
||||
el.detail_text,
|
||||
el.timestamp_x,
|
||||
vap_doctors.value AS doctors,
|
||||
vap_services.value AS services
|
||||
FROM b_iblock_element el
|
||||
JOIN b_file f ON f.id = el.preview_picture
|
||||
LEFT JOIN view_article_props vap_doctors ON el.id = vap_doctors.id AND vap_doctors.code = \'LINK_STAFF\'::text
|
||||
LEFT JOIN view_article_props vap_services ON el.id = vap_services.id AND vap_services.code = \'LINK_SERVICES\'::text
|
||||
WHERE el.iblock_id = ANY (ARRAY[69, 70, 71, 149, 179, 231])
|
||||
), grouped_articles AS (
|
||||
SELECT d.group_code,
|
||||
COALESCE(max(d.id), NULL::integer) AS id,
|
||||
COALESCE(NULLIF(TRIM(BOTH FROM max(d.name)), \'\'::text), NULL::text) AS name,
|
||||
COALESCE(max(
|
||||
CASE
|
||||
WHEN d.preview_picture IS NOT NULL THEN concat_ws(\'/\'::text, COALESCE(\'/upload\'::text, \'\'::text), COALESCE(d.subdir, \'\'::text), COALESCE(d.file_name, \'\'::text))
|
||||
ELSE NULL::text
|
||||
END), NULL::text) AS preview_picture,
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN max(
|
||||
CASE
|
||||
WHEN d.active = true THEN 1
|
||||
ELSE 0
|
||||
END) = 1 THEN true
|
||||
ELSE false
|
||||
END, NULL::boolean) AS active,
|
||||
CASE
|
||||
WHEN count(d.doctors) FILTER (WHERE d.doctors IS NOT NULL AND TRIM(BOTH FROM d.doctors) <> \'\'::text) > 0 THEN jsonb_agg(DISTINCT TRIM(BOTH FROM d.doctors)) FILTER (WHERE d.doctors IS NOT NULL AND TRIM(BOTH FROM d.doctors) <> \'\'::text)
|
||||
ELSE NULL::jsonb
|
||||
END AS doctors,
|
||||
CASE
|
||||
WHEN count(d.services) FILTER (WHERE d.services IS NOT NULL AND TRIM(BOTH FROM d.services) <> \'\'::text) > 0 THEN jsonb_agg(DISTINCT TRIM(BOTH FROM d.services)) FILTER (WHERE d.services IS NOT NULL AND TRIM(BOTH FROM d.services) <> \'\'::text)
|
||||
ELSE NULL::jsonb
|
||||
END AS services,
|
||||
COALESCE(max(
|
||||
CASE d.iblock_id
|
||||
WHEN 69 THEN 91
|
||||
WHEN 149 THEN 91
|
||||
WHEN 179 THEN 91
|
||||
WHEN 70 THEN 92
|
||||
WHEN 71 THEN 93
|
||||
WHEN 231 THEN 94
|
||||
ELSE d.iblock_id
|
||||
END), NULL::integer) AS region_id,
|
||||
COALESCE(max(d.code), NULL::text) AS alias,
|
||||
COALESCE(max(d.preview_text), NULL::text) AS anons,
|
||||
COALESCE(max(d.detail_text), NULL::text) AS content,
|
||||
COALESCE(max(d.timestamp_x), NULL::timestamp without time zone) AS update_at
|
||||
FROM article_data d
|
||||
GROUP BY d.group_code
|
||||
)
|
||||
SELECT ga.id,
|
||||
ga.name,
|
||||
ga.preview_picture,
|
||||
ga.active,
|
||||
ga.doctors,
|
||||
ga.services,
|
||||
ga.region_id,
|
||||
ga.alias,
|
||||
ga.anons,
|
||||
ga.content,
|
||||
ga.update_at
|
||||
FROM grouped_articles ga
|
||||
ORDER BY ga.id');
|
||||
|
||||
// Наполняем таблицу article данными из представления
|
||||
$this->addSql('INSERT INTO article (id, name, preview_picture, active, doctors, services, region_id, alias, anons, content, update_at)
|
||||
SELECT id, name, preview_picture, active, doctors, services, region_id, alias, anons, content, update_at
|
||||
FROM view_article');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('TRUNCATE TABLE article');
|
||||
$this->addSql('DROP VIEW IF EXISTS public.view_article');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260311212936 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE medical_center (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, alias VARCHAR(255) DEFAULT NULL, anons TEXT DEFAULT NULL, content TEXT DEFAULT NULL, kod_uslug JSONB DEFAULT NULL, doctors JSONB DEFAULT NULL, services JSONB DEFAULT NULL, articles JSONB DEFAULT NULL, txt_up TEXT DEFAULT NULL, PRIMARY KEY (id))');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE medical_center');
|
||||
$this->addSql('ALTER TABLE article ALTER doctors TYPE JSONB');
|
||||
$this->addSql('ALTER TABLE article ALTER services TYPE JSONB');
|
||||
$this->addSql('CREATE INDEX idx_article_region_id ON article (region_id)');
|
||||
$this->addSql('CREATE INDEX idx_article_active ON article (active)');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260417120000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add department column to specialist_dcode_description';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE specialist_dcode_description ADD department BIGINT DEFAULT NULL');
|
||||
$this->addSql('CREATE INDEX idx_specialist_dcode_description_department ON specialist_dcode_description (department)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX idx_specialist_dcode_description_department');
|
||||
$this->addSql('ALTER TABLE specialist_dcode_description DROP department');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260515142000 extends AbstractMigration
|
||||
{
|
||||
private const TABLES = [
|
||||
'news',
|
||||
'promo',
|
||||
'disease',
|
||||
'medical_center',
|
||||
'site_services',
|
||||
];
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add generated id defaults for content CRUD entities';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
foreach (self::TABLES as $table) {
|
||||
$sequence = $table . '_id_seq';
|
||||
|
||||
$this->addSql(sprintf('CREATE SEQUENCE IF NOT EXISTS %s OWNED BY %s.id', $sequence, $table));
|
||||
$this->addSql(sprintf(
|
||||
'SELECT setval(\'%s\', COALESCE((SELECT MAX(id) FROM %s), 0) + 1, false)',
|
||||
$sequence,
|
||||
$table,
|
||||
));
|
||||
$this->addSql(sprintf(
|
||||
'ALTER TABLE %s ALTER COLUMN id SET DEFAULT nextval(\'%s\')',
|
||||
$table,
|
||||
$sequence,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
foreach (array_reverse(self::TABLES) as $table) {
|
||||
$sequence = $table . '_id_seq';
|
||||
|
||||
$this->addSql(sprintf('ALTER TABLE %s ALTER COLUMN id DROP DEFAULT', $table));
|
||||
$this->addSql(sprintf('DROP SEQUENCE IF EXISTS %s', $sequence));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<php>
|
||||
<ini name="display_errors" value="1" />
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<server name="APP_ENV" value="test" force="true" />
|
||||
<server name="SHELL_VERBOSITY" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true" restrictNotices="true" restrictWarnings="true">
|
||||
<include>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
</source>
|
||||
|
||||
<extensions>
|
||||
</extensions>
|
||||
</phpunit>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"openapi": "3.0.0"
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use App\Entity\Specialist;
|
||||
use App\Service\Bitrix\BitrixService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'bitrix-update-doctors',
|
||||
description: 'Обновления врачей из CMS Bitrix',
|
||||
)]
|
||||
class BitrixUpdateDoctorsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private BitrixService $bitrixService
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$this->logger->info('start: bitrix-update-doctors');
|
||||
$io->info('Началось выполнение.');
|
||||
|
||||
$specialistList = $this->entityManager->getRepository(Specialist::class)->findAll();
|
||||
$count = 0;
|
||||
|
||||
foreach ($specialistList as $specialist) {
|
||||
$dcodes = $specialist->getDcodes();
|
||||
|
||||
if (empty($dcodes) || $dcodes === '0' || $dcodes === 0) {
|
||||
$specialist->setDcodes(null);
|
||||
} else {
|
||||
$dcodesArray = explode(',', $dcodes);
|
||||
|
||||
$filteredDcodes = array_filter(
|
||||
array_unique($dcodesArray),
|
||||
function($item) {
|
||||
return strlen(trim($item)) >= 7 && trim($item) !== '0';
|
||||
}
|
||||
);
|
||||
|
||||
if (empty($filteredDcodes)) {
|
||||
$specialist->setDcodes(null);
|
||||
} else {
|
||||
$specialist->setDcodes(implode(',', $filteredDcodes));
|
||||
}
|
||||
}
|
||||
|
||||
// $kodoper = $this->bitrixService->getServiceCode($specialist->getId());
|
||||
|
||||
// if (!empty($kodoper)) {
|
||||
// $specialist->setKodoper($kodoper);
|
||||
// $this->entityManager->persist($specialist);
|
||||
// }
|
||||
|
||||
$count ++;
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
$this->logger->info('end: bitrix-update-doctors ' . $count);
|
||||
$io->success('load: ' . $count);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use App\Entity\Specialist;
|
||||
use App\Entity\Review;
|
||||
use App\Service\Bitrix\BitrixService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
|
||||
|
||||
#[AsCommand(
|
||||
name: 'bitrix-update-reviews',
|
||||
description: 'Обновления отзывов врачей из CMS Bitrix',
|
||||
)]
|
||||
final class BitrixUpdateReviewsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private BitrixService $bitrixService
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private $review = [
|
||||
'source' => null,
|
||||
'rating' => 5
|
||||
];
|
||||
|
||||
private function safeUtf8Clean($text)
|
||||
{
|
||||
$result = '';
|
||||
$length = strlen($text);
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$byte = ord($text[$i]);
|
||||
|
||||
// Валидные ASCII символы
|
||||
if ($byte <= 0x7F) {
|
||||
// Разрешаем только печатаемые ASCII и управляющие символы
|
||||
if ($byte >= 0x20 || $byte == 0x09 || $byte == 0x0A || $byte == 0x0D) {
|
||||
$result .= $text[$i];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Многобайтовые UTF-8 последовательности
|
||||
if (($byte & 0xE0) == 0xC0) {
|
||||
// 2-байтовая последовательность
|
||||
if ($i + 1 < $length) {
|
||||
$byte2 = ord($text[$i + 1]);
|
||||
if (($byte2 & 0xC0) == 0x80) {
|
||||
$result .= $text[$i] . $text[$i + 1];
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
} elseif (($byte & 0xF0) == 0xE0) {
|
||||
// 3-байтовая последовательность
|
||||
if ($i + 2 < $length) {
|
||||
$byte2 = ord($text[$i + 1]);
|
||||
$byte3 = ord($text[$i + 2]);
|
||||
if (($byte2 & 0xC0) == 0x80 && ($byte3 & 0xC0) == 0x80) {
|
||||
$result .= $text[$i] . $text[$i + 1] . $text[$i + 2];
|
||||
$i += 2;
|
||||
}
|
||||
}
|
||||
} elseif (($byte & 0xF8) == 0xF0) {
|
||||
// 4-байтовая последовательность
|
||||
if ($i + 3 < $length) {
|
||||
$byte2 = ord($text[$i + 1]);
|
||||
$byte3 = ord($text[$i + 2]);
|
||||
$byte4 = ord($text[$i + 3]);
|
||||
if (($byte2 & 0xC0) == 0x80 && ($byte3 & 0xC0) == 0x80 && ($byte4 & 0xC0) == 0x80) {
|
||||
$result .= $text[$i] . $text[$i + 1] . $text[$i + 2] . $text[$i + 3];
|
||||
$i += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function processName(array $data): void
|
||||
{
|
||||
$name = $this->safeUtf8Clean($data['VALUE']);
|
||||
|
||||
$this->review['name'] = empty($name)? 'Анонимно': $name;
|
||||
$this->review['dateCreate'] = \DateTime::createFromFormat('Y-m-d H:i:s', $data['DATE_CREATE']);
|
||||
$this->review['active'] = ($data['ACTIVE'] == 'Y')? true: false;
|
||||
}
|
||||
|
||||
private function processMessage(string $value): void
|
||||
{
|
||||
$message = $this->safeUtf8Clean($value);
|
||||
|
||||
$message = preg_replace('/a:\d+:\{.*\}|s:\d+:".*"|\w+:\d+:/', '', $message);
|
||||
$message = str_replace('{', '', $message);
|
||||
$message = str_replace('}', '', $message);
|
||||
|
||||
$this->review['message'] = trim(strip_tags($message));
|
||||
}
|
||||
|
||||
private function processSourceLink(string $value): void
|
||||
{
|
||||
preg_match('/https?:\/\/[^\s<>"\']+/i', $value, $matches);
|
||||
|
||||
if (!empty($matches[0])) {
|
||||
$this->review['source'] = $this->safeUtf8Clean(substr($matches[0], 0, 255));
|
||||
}
|
||||
}
|
||||
|
||||
private function processRating($value): void
|
||||
{
|
||||
$this->review['rating'] = max(1, min(5, $value));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->info('Началось выполнение.');
|
||||
|
||||
$this->logger->info('start: bitrix-update-reviews');
|
||||
|
||||
$specialistAll = $this->entityManager->getRepository(Specialist::class)->findAll();
|
||||
$progressBar = new ProgressBar($output, count($specialistAll));
|
||||
|
||||
unset($specialistAll);
|
||||
|
||||
$page = 1;
|
||||
$batchSize = 5;
|
||||
|
||||
do {
|
||||
$progressBar->advance();
|
||||
$query = $this->entityManager->getRepository(Specialist::class)
|
||||
->createQueryBuilder('s')
|
||||
->setFirstResult(($page - 1) * $batchSize)
|
||||
->setMaxResults($batchSize)
|
||||
->getQuery();
|
||||
|
||||
$paginator = new Paginator($query);
|
||||
$specialistList = iterator_to_array($paginator);
|
||||
$page++;
|
||||
|
||||
$this->loadData($specialistList);
|
||||
$this->entityManager->clear();
|
||||
|
||||
gc_collect_cycles();
|
||||
} while (count($specialistList) > 0);
|
||||
|
||||
$this->entityManager->clear();
|
||||
$progressBar->finish();
|
||||
$output->writeln('');
|
||||
|
||||
gc_collect_cycles();
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function loadData(array $specialistList)
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($specialistList as $specialist) {
|
||||
|
||||
$reviews = $this->bitrixService->getReviews($specialist->getId());
|
||||
|
||||
foreach ($reviews as $key => $params) {
|
||||
$this->review['externalId'] = (int) $params['REVIEW_ID'];
|
||||
|
||||
$review = $this->entityManager->getRepository(Review::class)
|
||||
->findOneBy(['externalId' => $this->review['externalId']]);
|
||||
|
||||
foreach ($params['DATA'] as $data) {
|
||||
$code = $data['CODE'];
|
||||
$value = $data['VALUE'];
|
||||
|
||||
match ($code) {
|
||||
"NAME" => $this->processName($data),
|
||||
"MESSAGE" => $this->processMessage($value),
|
||||
"SOURCE_LINK" => $this->processSourceLink($value),
|
||||
"RATING" => $this->processRating((int) $value),
|
||||
default => null // Игнорируем неизвестные коды
|
||||
};
|
||||
}
|
||||
|
||||
if (!$this->review['active'] || empty($this->review['message'])) continue;
|
||||
|
||||
if (!$review) {
|
||||
$review = new Review;
|
||||
$review->setExternalId($this->review['externalId']);
|
||||
}
|
||||
|
||||
$review
|
||||
->setActive($this->review['active'])
|
||||
->setDateCreate($this->review['dateCreate'])
|
||||
->setSource($this->review['source'])
|
||||
->setRating($this->review['rating'])
|
||||
->setAuthor($this->review['name'])
|
||||
->setMessage($this->review['message']);
|
||||
|
||||
$specialist->addReview($review);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->entityManager->flush();
|
||||
} catch (DriverException $e) {
|
||||
$this->logger->error('Problematic parameters: ' . print_r($this->review, true));
|
||||
|
||||
// Проверяем каждый параметр на валидность UTF-8
|
||||
foreach ($this->review as $index => $param) {
|
||||
if (is_string($param) && !mb_check_encoding($param, 'UTF-8')) {
|
||||
$this->logger->error("Invalid UTF-8 in parameter $index: " . bin2hex($param));
|
||||
}
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$count ++;
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\ScheduleCache\ScheduleCacheService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:schedule:clear-cache',
|
||||
description: 'Clear old schedule cache'
|
||||
)]
|
||||
class ClearScheduleCacheCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private ScheduleCacheService $cacheService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('hours', null, InputOption::VALUE_REQUIRED, 'Clear cache older than X hours', 24)
|
||||
->addOption('stats', null, InputOption::VALUE_NONE, 'Show cache statistics')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if ($input->getOption('stats')) {
|
||||
$stats = $this->cacheService->getCacheStats();
|
||||
|
||||
$io->title('Schedule Cache Statistics');
|
||||
$io->table(
|
||||
['Metric', 'Value'],
|
||||
[
|
||||
['Total Records', number_format($stats['total_records'])],
|
||||
['Unique Queries', number_format($stats['unique_queries'])],
|
||||
['Oldest Record', $stats['oldest_record']->format('Y-m-d H:i:s')],
|
||||
['Newest Record', $stats['newest_record']->format('Y-m-d H:i:s')]
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($stats['last_7_days'])) {
|
||||
$io->section('Last 7 Days Activity');
|
||||
$rows = [];
|
||||
foreach ($stats['last_7_days'] as $day) {
|
||||
$rows[] = [
|
||||
$day['day'],
|
||||
number_format($day['records_count']),
|
||||
number_format($day['queries_count'])
|
||||
];
|
||||
}
|
||||
$io->table(['Date', 'Records', 'Queries'], $rows);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$hours = (int)$input->getOption('hours');
|
||||
$olderThan = new \DateTime(sprintf('-%d hours', $hours));
|
||||
|
||||
$io->note(sprintf('Clearing cache older than %s', $olderThan->format('Y-m-d H:i:s')));
|
||||
|
||||
try {
|
||||
$deletedCount = $this->cacheService->clearOldCache($olderThan);
|
||||
|
||||
$io->success(sprintf('Successfully cleared %d cache records older than %d hours', $deletedCount, $hours));
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$io->error(sprintf('Error clearing cache: %s', $e->getMessage()));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Department;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use App\Service\Client\Interfaces\InfoclinicaClientServiceInterface;
|
||||
use App\Service\Translite\Interfaces\TransliteServiceInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:deps',
|
||||
description: 'Пакетное обновление отделений для врачей',
|
||||
)]
|
||||
class UploadDepartmentsCommand extends Command
|
||||
{
|
||||
private const BATCH_SIZE = 100;
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private InfoclinicaClientServiceInterface $client,
|
||||
private TransliteServiceInterface $transliteService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void { }
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Пакетное обновление отделений');
|
||||
|
||||
try {
|
||||
// Используем InfoclinicaClientServiceInterface для запроса
|
||||
$httpResponse = $this->client->request('GET', '/specialists/departments');
|
||||
$responseData = $httpResponse->toArray();
|
||||
|
||||
if (empty($responseData['data'])) {
|
||||
$io->success('Нет данных для обработки');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$totalRecords = count($responseData['data']);
|
||||
$io->info("Загружено записей из API: {$totalRecords}");
|
||||
|
||||
// Убираем возможные дубликаты из данных API (по did)
|
||||
$uniqueData = $this->removeDuplicates($responseData['data']);
|
||||
$uniqueCount = count($uniqueData);
|
||||
|
||||
if ($uniqueCount < $totalRecords) {
|
||||
$io->note("Удалено дубликатов: " . ($totalRecords - $uniqueCount));
|
||||
$io->info("Уникальных записей для обработки: {$uniqueCount}");
|
||||
}
|
||||
|
||||
// Пакетная обработка с UPSERT
|
||||
$processed = $this->processWithUpsert($uniqueData, $io);
|
||||
|
||||
// Статистика
|
||||
$io->table(
|
||||
['Статистика', 'Значение'],
|
||||
[
|
||||
['Всего записей в API', $totalRecords],
|
||||
['Уникальных записей', $uniqueCount],
|
||||
['Успешно обработано', $processed],
|
||||
['Пропущено', $uniqueCount - $processed]
|
||||
]
|
||||
);
|
||||
|
||||
$io->success('Обработка завершена успешно');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$io->error('Ошибка: ' . $e->getMessage());
|
||||
$io->error('Trace: ' . $e->getTraceAsString());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет дубликаты по полю id (did)
|
||||
*/
|
||||
private function removeDuplicates(array $data): array
|
||||
{
|
||||
$unique = [];
|
||||
foreach ($data as $item) {
|
||||
if (!isset($item['id'])) {
|
||||
continue;
|
||||
}
|
||||
$unique[$item['id']] = $item; // id из API как ключ для уникальности
|
||||
}
|
||||
return array_values($unique);
|
||||
}
|
||||
|
||||
/**
|
||||
* Основной метод обработки с UPSERT
|
||||
* Возвращает количество успешно обработанных записей
|
||||
*/
|
||||
private function processWithUpsert(array $data, SymfonyStyle $io): int
|
||||
{
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$tableName = $this->entityManager->getClassMetadata(Department::class)->getTableName();
|
||||
|
||||
$total = count($data);
|
||||
$processed = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$io->progressStart($total);
|
||||
|
||||
// Обрабатываем пакетами
|
||||
for ($i = 0; $i < $total; $i += self::BATCH_SIZE) {
|
||||
$batch = array_slice($data, $i, self::BATCH_SIZE);
|
||||
|
||||
try {
|
||||
$batchProcessed = $this->executeUpsertBatch($connection, $tableName, $batch, $io);
|
||||
$processed += $batchProcessed;
|
||||
|
||||
$io->progressAdvance(count($batch));
|
||||
|
||||
// Периодически выводим статистику
|
||||
if ($i % (self::BATCH_SIZE * 10) === 0 || $i + self::BATCH_SIZE >= $total) {
|
||||
$io->writeln(sprintf(
|
||||
' [%s] Обработано: %d/%d (%.1f%%)',
|
||||
date('H:i:s'),
|
||||
$processed,
|
||||
$total,
|
||||
($processed / $total) * 100
|
||||
));
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$io->warning(sprintf(
|
||||
'Ошибка в пакете %d-%d: %s',
|
||||
$i + 1,
|
||||
min($i + self::BATCH_SIZE, $total),
|
||||
$e->getMessage()
|
||||
));
|
||||
|
||||
// Если ошибка в пакете, пробуем обработать по одной записи
|
||||
$batchProcessed = $this->handleBatchError($connection, $tableName, $batch, $io);
|
||||
$processed += $batchProcessed;
|
||||
$skipped += (count($batch) - $batchProcessed);
|
||||
}
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
if ($skipped > 0) {
|
||||
$io->warning("Пропущено записей из-за ошибок: {$skipped}");
|
||||
}
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет UPSERT для пакета данных
|
||||
* Возвращает количество успешно обработанных записей в пакете
|
||||
*/
|
||||
private function executeUpsertBatch($connection, string $tableName, array $batch, SymfonyStyle $io): int
|
||||
{
|
||||
if (empty($batch)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$sqlParts = [];
|
||||
$params = [];
|
||||
$validItems = 0;
|
||||
|
||||
foreach ($batch as $index => $item) {
|
||||
if (!isset($item['id']) || !isset($item['name'])) {
|
||||
continue; // Пропускаем некорректные записи
|
||||
}
|
||||
|
||||
$sqlParts[] = sprintf(
|
||||
'(:did_%d, :name_%d, :alias_%d, :active_%d, :online_mode_%d)',
|
||||
$index, $index, $index, $index, $index
|
||||
);
|
||||
|
||||
$params['did_' . $index] = (int) $item['id'];
|
||||
$params['name_' . $index] = $item['name'];
|
||||
$params['alias_' . $index] = $this->transliteService->translit($item['name']);
|
||||
$params['active_' . $index] = true;
|
||||
$params['online_mode_' . $index] = $item['onlineMode'] ?? false;
|
||||
|
||||
$validItems++;
|
||||
}
|
||||
|
||||
if (empty($sqlParts)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$valuesSql = implode(', ', $sqlParts);
|
||||
$updateSql = sprintf(
|
||||
'UPDATE %1$s AS d
|
||||
SET
|
||||
name = src.name,
|
||||
alias = src.alias,
|
||||
active = CAST(src.active AS BOOLEAN),
|
||||
online_mode = CAST(src.online_mode AS BOOLEAN)
|
||||
FROM (VALUES %2$s) AS src(did, name, alias, active, online_mode)
|
||||
WHERE d.did = CAST(src.did AS BIGINT)',
|
||||
$tableName,
|
||||
$valuesSql
|
||||
);
|
||||
|
||||
$insertSql = sprintf(
|
||||
'INSERT INTO %1$s (did, name, alias, active, online_mode)
|
||||
SELECT
|
||||
CAST(src.did AS BIGINT),
|
||||
src.name,
|
||||
src.alias,
|
||||
CAST(src.active AS BOOLEAN),
|
||||
CAST(src.online_mode AS BOOLEAN)
|
||||
FROM (VALUES %2$s) AS src(did, name, alias, active, online_mode)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM %1$s d WHERE d.did = CAST(src.did AS BIGINT)
|
||||
)',
|
||||
$tableName,
|
||||
$valuesSql
|
||||
);
|
||||
|
||||
$connection->executeStatement($updateSql, $params);
|
||||
$connection->executeStatement($insertSql, $params);
|
||||
|
||||
return $validItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает ошибку пакета, пробуя вставить записи по одной
|
||||
* Возвращает количество успешно обработанных записей
|
||||
*/
|
||||
private function handleBatchError($connection, string $tableName, array $batch, SymfonyStyle $io): int
|
||||
{
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($batch as $item) {
|
||||
try {
|
||||
if ($this->executeSingleUpsert($connection, $tableName, $item)) {
|
||||
$successCount++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$io->warning(sprintf(
|
||||
'Не удалось обработать запись did=%s: %s',
|
||||
$item['id'] ?? 'unknown',
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $successCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет UPSERT для одной записи
|
||||
* Возвращает true если успешно
|
||||
*/
|
||||
private function executeSingleUpsert($connection, string $tableName, array $item): bool
|
||||
{
|
||||
if (!isset($item['id']) || !isset($item['name'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$alias = $this->transliteService->translit($item['name']);
|
||||
$active = $item['active'] ?? true;
|
||||
$onlineMode = $item['onlineMode'] ?? false;
|
||||
|
||||
$updateSql = sprintf(
|
||||
'UPDATE %s
|
||||
SET
|
||||
name = :name,
|
||||
alias = :alias,
|
||||
active = :active,
|
||||
online_mode = :online_mode
|
||||
WHERE did = :did',
|
||||
$tableName
|
||||
);
|
||||
|
||||
$insertSql = sprintf(
|
||||
'INSERT INTO %s (did, name, alias, active, online_mode)
|
||||
SELECT :did, :name, :alias, :active, :online_mode
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM %s WHERE did = :did
|
||||
)',
|
||||
$tableName,
|
||||
$tableName
|
||||
);
|
||||
|
||||
$params = [
|
||||
'did' => (int) $item['id'],
|
||||
'name' => $item['name'],
|
||||
'alias' => $alias,
|
||||
'active' => $active,
|
||||
'online_mode' => $onlineMode
|
||||
];
|
||||
|
||||
$connection->executeStatement($updateSql, $params);
|
||||
$connection->executeStatement($insertSql, $params);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\DiseaseCrudService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:diseases',
|
||||
description: 'Обновление таблицы disease из view_disease',
|
||||
)]
|
||||
final class UploadDiseasesCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private DiseaseCrudService $diseaseCrudService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('view', null, InputOption::VALUE_OPTIONAL, 'SQL view name', 'public.view_disease');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$viewName = $input->getOption('view');
|
||||
if (empty($viewName)) {
|
||||
$viewName = 'public.view_disease';
|
||||
}
|
||||
$viewName = (string) $viewName;
|
||||
|
||||
$io->title('Disease: sync from view_disease');
|
||||
|
||||
try {
|
||||
$this->logger->info('Disease sync start', ['view' => $viewName]);
|
||||
$affected = $this->diseaseCrudService->syncFromViewDisease($viewName);
|
||||
$io->success('Sync finished. Affected rows: ' . $affected);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Disease sync failed', [
|
||||
'view' => $viewName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$io->error('Ошибка: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Idoctor;
|
||||
use App\Entity\Department;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use App\Service\Client\Interfaces\InfoclinicaClientServiceInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:doctors',
|
||||
description: 'Пакетное обновление врачей из инфоклиники',
|
||||
)]
|
||||
class UploadDoctorsCommand extends Command
|
||||
{
|
||||
private const BATCH_SIZE = 150;
|
||||
private const CHUNK_SIZE = 300;
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private InfoclinicaClientServiceInterface $client
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('onlineMode', InputArgument::OPTIONAL, 'Режим онлайн (0 или 1)', 0);
|
||||
$this->addOption('department', 'd', InputOption::VALUE_OPTIONAL, 'ID конкретного отделения');
|
||||
$this->addOption('firstrow', 'f', InputOption::VALUE_OPTIONAL, 'Первая строка', 1);
|
||||
$this->addOption('lastrow', 'l', InputOption::VALUE_OPTIONAL, 'Последняя строка', 900);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Пакетное обновление врачей');
|
||||
|
||||
try {
|
||||
$onlineMode = (bool) $input->getArgument('onlineMode');
|
||||
$departmentId = (int) $input->getOption('department');
|
||||
$firstRow = (int) $input->getOption('firstrow');
|
||||
$lastRow = (int) $input->getOption('lastrow');
|
||||
|
||||
$departments = $this->getDepartmentsToProcess($departmentId);
|
||||
|
||||
if (empty($departments)) {
|
||||
$io->warning('Не найдено отделений для обработки');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info('Найдено отделений: ' . count($departments));
|
||||
$io->info('Режим онлайн: ' . ($onlineMode ? 'Да' : 'Нет'));
|
||||
|
||||
$totalDoctorsProcessed = 0;
|
||||
|
||||
foreach ($departments as $index => $department) {
|
||||
$io->section('Обработка отделения: ' . $department['name'] . ' (ID: ' . $department['did'] . ')');
|
||||
|
||||
$doctorsData = $this->fetchDoctorsData($department['did'], $onlineMode, $firstRow, $lastRow, $io);
|
||||
|
||||
if (empty($doctorsData)) {
|
||||
$io->note('Нет данных врачей для отделения');
|
||||
continue;
|
||||
}
|
||||
|
||||
$processed = $this->processDoctorsBatch($doctorsData, $department['did'], $onlineMode, $io);
|
||||
$totalDoctorsProcessed += $processed;
|
||||
$io->writeln(sprintf(
|
||||
'Обработано врачей в отделении: %d',
|
||||
$processed
|
||||
));
|
||||
|
||||
if ($index < count($departments) - 1) {
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
$this->entityManager->clear();
|
||||
}
|
||||
|
||||
$io->success(sprintf('Обработка завершена. Всего обработано врачей: %d', $totalDoctorsProcessed));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$io->error('Ошибка: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function getDepartmentsToProcess(?int $departmentId): array
|
||||
{
|
||||
$repository = $this->entityManager->getRepository(Department::class);
|
||||
$qb = $repository->createQueryBuilder('d')
|
||||
->select('d.did AS did, d.name AS name')
|
||||
->orderBy('d.id', 'ASC');
|
||||
|
||||
if ($departmentId) {
|
||||
$qb->andWhere('d.did = :departmentId')
|
||||
->setParameter('departmentId', $departmentId);
|
||||
} else {
|
||||
$qb->andWhere('d.active = :active')
|
||||
->setParameter('active', true);
|
||||
}
|
||||
|
||||
$departments = $qb->getQuery()->getArrayResult();
|
||||
|
||||
return array_map(static fn (array $department): array => [
|
||||
'did' => (int) $department['did'],
|
||||
'name' => (string) $department['name'],
|
||||
], $departments);
|
||||
}
|
||||
|
||||
private function fetchDoctorsData(int $departmentId, bool $onlineMode, int $firstRow, int $lastRow, SymfonyStyle $io): array
|
||||
{
|
||||
$allDoctors = [];
|
||||
$chunkSize = self::CHUNK_SIZE;
|
||||
|
||||
for ($start = $firstRow; $start <= $lastRow; $start += $chunkSize) {
|
||||
$end = min($start + $chunkSize - 1, $lastRow);
|
||||
|
||||
$path = sprintf(
|
||||
'/specialists/doctors?departments=%d&onlineMode=%d&firstrow=%d&lastrow=%d',
|
||||
$departmentId,
|
||||
$onlineMode ? 1 : 0,
|
||||
$start,
|
||||
$end
|
||||
);
|
||||
|
||||
try {
|
||||
$httpResponse = $this->client->request('GET', $path);
|
||||
$response = $httpResponse->toArray() ?: [];
|
||||
|
||||
if (!empty($response['data'])) {
|
||||
$allDoctors = array_merge($allDoctors, $response['data']);
|
||||
$io->writeln(sprintf('Загружено врачей: %d (%d-%d)', count($response['data']), $start, $end));
|
||||
}
|
||||
|
||||
if (count($response['data'] ?? []) < $chunkSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(200000);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$io->warning('Ошибка при загрузке данных для отделения ' . $departmentId . ': ' . $e->getMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $allDoctors;
|
||||
}
|
||||
|
||||
private function processDoctorsBatch(array $doctorsData, int $departmentId, bool $onlineMode, SymfonyStyle $io): int
|
||||
{
|
||||
if (empty($doctorsData)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// Получаем существующих врачей
|
||||
$existingDoctors = $this->getExistingDoctors($doctorsData, $departmentId, $onlineMode);
|
||||
|
||||
$processed = 0;
|
||||
$batchCount = 0;
|
||||
|
||||
foreach ($doctorsData as $index => $doctorData) {
|
||||
try {
|
||||
$doctorKey = $this->getDoctorKey($doctorData['dcode'], $departmentId, $onlineMode);
|
||||
|
||||
$iDoctor = $existingDoctors[$doctorKey] ?? new Idoctor();
|
||||
|
||||
$this->updateDoctorEntity($iDoctor, $doctorData, $departmentId, $onlineMode);
|
||||
|
||||
$this->entityManager->persist($iDoctor);
|
||||
$processed++;
|
||||
$batchCount++;
|
||||
|
||||
if ($batchCount % self::BATCH_SIZE === 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->writeln(sprintf('Сохранено пакет: %d врачей', self::BATCH_SIZE));
|
||||
$batchCount = 0;
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$io->warning('Ошибка при обработке врача ' . ($doctorData['dcode'] ?? 'unknown') . ': ' . $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($batchCount > 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->writeln(sprintf('Сохранено остальных: %d врачей', $batchCount));
|
||||
}
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает существующих врачей
|
||||
*/
|
||||
private function getExistingDoctors(array $doctorsData, int $departmentId, bool $onlineMode): array
|
||||
{
|
||||
$dCodes = array_column($doctorsData, 'dcode');
|
||||
|
||||
if (empty($dCodes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$existingDoctors = $this->entityManager->getRepository(Idoctor::class)
|
||||
->createQueryBuilder('d')
|
||||
->where('d.dcode IN (:dCodes)')
|
||||
->andWhere('d.department = :department')
|
||||
->andWhere('d.onlineMode = :onlineMode')
|
||||
->setParameter('dCodes', $dCodes)
|
||||
->setParameter('department', $departmentId)
|
||||
->setParameter('onlineMode', $onlineMode)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
$result = [];
|
||||
foreach ($existingDoctors as $doctor) {
|
||||
$key = $this->getDoctorKey(
|
||||
$doctor->getDcode(),
|
||||
$doctor->getDepartment(),
|
||||
$doctor->getOnlineMode()
|
||||
);
|
||||
$result[$key] = $doctor;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает уникальный ключ для врача
|
||||
*/
|
||||
private function getDoctorKey(string $dcode, int $departmentId, bool $onlineMode): string
|
||||
{
|
||||
return sprintf('%s_%d_%d', $dcode, $departmentId, $onlineMode ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет сущность врача
|
||||
*/
|
||||
private function updateDoctorEntity(Idoctor $doctor, array $data, int $departmentId, bool $onlineMode): void
|
||||
{
|
||||
$doctor
|
||||
->setDcode($data['dcode'] ?? '')
|
||||
->setName($data['name'] ?? '')
|
||||
->setDepartment($departmentId)
|
||||
->setFilial($data['filialId'] ?? null)
|
||||
->setNearestDate($data['nearestDate'] ?? null)
|
||||
->setOnlineMode($onlineMode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Filial;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use App\Service\Translite\Interfaces\TransliteServiceInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:filials',
|
||||
description: 'Обнвление филиалов',
|
||||
)]
|
||||
class UploadFilialsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private HttpClientInterface $client,
|
||||
private TransliteServiceInterface $transliteService,
|
||||
private string $widgetApiUrl,
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void { }
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->logger->info('Началось обнвление филиалов');
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$response = $this->client->request('GET', '/filials/list', [
|
||||
'verify_peer' => false,
|
||||
'verify_host' => false,
|
||||
'base_uri' => $this->widgetApiUrl,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'sovamed_bot'
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $response->toArray();
|
||||
|
||||
$io->info('load:' . count($response['data']));
|
||||
foreach ($response['data'] as $item) {
|
||||
$filial = $this->entityManager->getRepository(Filial::class)
|
||||
->findOneBy(['fid' => $item['id']]);
|
||||
|
||||
if (is_null($filial)) {
|
||||
$filial = new Filial();
|
||||
}
|
||||
|
||||
preg_match('/(ул\.\s*[А-я]+(?:\s+[А-я]+)*)\s*,?\s*д\.\s*(\S+)/u', $item['address'], $matches);
|
||||
|
||||
if (isset($matches[1]) && isset($matches[2])) {
|
||||
$street = $matches[1];
|
||||
$house = $matches[2];
|
||||
|
||||
$filial->setShortName("$street,$house");
|
||||
}
|
||||
|
||||
$filial
|
||||
->setFid($item['id'])
|
||||
->setName($item['name'])
|
||||
->setAddress($item['address'])
|
||||
->setActive(true)
|
||||
->setRegionId($this->findRegionId($item['address']))
|
||||
;
|
||||
|
||||
$this->entityManager->persist($filial);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
$io->success('loaded');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function findRegionId($address) {
|
||||
$cities = [
|
||||
91 => "Саратов",
|
||||
92 => "Волгоград",
|
||||
93 => "Воронеж",
|
||||
94 => "Краснодар",
|
||||
];
|
||||
|
||||
foreach ($cities as $key => $city) {
|
||||
if (stripos($address, $city) !== false) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\MedicalCenterCrudService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:medical-centers',
|
||||
description: 'Обновление таблицы medical_center из view_centers'
|
||||
)]
|
||||
final class UploadMedicalCentersCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private MedicalCenterCrudService $medicalCenterCrudService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('view', null, InputOption::VALUE_OPTIONAL, 'SQL view name', 'public.view_centers');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$viewName = $input->getOption('view');
|
||||
if (empty($viewName)) {
|
||||
$viewName = 'public.view_centers';
|
||||
}
|
||||
$viewName = (string) $viewName;
|
||||
|
||||
$io->title('MedicalCenter: sync from view_centers');
|
||||
|
||||
try {
|
||||
$this->logger->info('MedicalCenter sync start', ['view' => $viewName]);
|
||||
$affected = $this->medicalCenterCrudService->syncFromViewCenters($viewName);
|
||||
$io->success('Sync finished. Affected rows: ' . $affected);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('MedicalCenter sync failed', [
|
||||
'view' => $viewName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$io->error('Ошибка: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\NewsCrudService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:news',
|
||||
description: 'Обновление таблицы news из view_news'
|
||||
)]
|
||||
final class UploadNewsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private NewsCrudService $newsCrudService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('view', null, InputOption::VALUE_OPTIONAL, 'SQL view name', 'public.view_news');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$viewName = $input->getOption('view');
|
||||
if (empty($viewName)) {
|
||||
$viewName = 'public.view_news';
|
||||
}
|
||||
$viewName = (string) $viewName;
|
||||
|
||||
$io->title('News: sync from view_news');
|
||||
|
||||
try {
|
||||
$this->logger->info('News sync start', ['view' => $viewName]);
|
||||
$affected = $this->newsCrudService->syncFromViewNews($viewName);
|
||||
$io->success('Sync finished. Affected rows: ' . $affected);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('News sync failed', [
|
||||
'view' => $viewName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$io->error('Ошибка: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\PriceDepartment;
|
||||
use App\Entity\PriceList;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:price',
|
||||
description: 'Обновление цен',
|
||||
)]
|
||||
class UploadPriceCommand extends Command
|
||||
{
|
||||
private $nosleep;
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private HttpClientInterface $client,
|
||||
private string $widgetApiUrl,
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('did', InputArgument::OPTIONAL, 'department id update')
|
||||
->addOption('debug', null, InputOption::VALUE_NONE, 'debug on')
|
||||
->addOption('nosleep', null, InputOption::VALUE_OPTIONAL, 'sleep true|false default false')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$departments = $this->entityManager->getRepository(PriceDepartment::class)->findAll();
|
||||
$departmentId = $input->getArgument('did');
|
||||
$debug = $input->getOption('debug');
|
||||
$this->nosleep = $input->getOption('nosleep');
|
||||
|
||||
if ($departmentId) {
|
||||
$departments = $this->entityManager->getRepository(PriceDepartment::class)
|
||||
->findBy(['groupId' => $departmentId]);
|
||||
}
|
||||
|
||||
if (empty($departments)) {
|
||||
$io->error('No departments found');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$batchSize = 100;
|
||||
$processedCount = 0;
|
||||
|
||||
foreach ($departments as $department) {
|
||||
$io->note("Processing department: " . $department->getGroupId());
|
||||
|
||||
foreach ($this->getPricelist($department->getGroupId()) as $collection) {
|
||||
foreach ($collection as $item) {
|
||||
$priceList = $this->entityManager->getRepository(PriceList::class)
|
||||
->findOneBy([
|
||||
'kodoper' => $item['kodoper'],
|
||||
'speccode' => $item['speccode'],
|
||||
'filial' => $item['filial'],
|
||||
'groupId' => $department->getGroupId()
|
||||
]);
|
||||
|
||||
$text = 'update: ';
|
||||
|
||||
if (is_null($priceList)) {
|
||||
$priceList = new PriceList();
|
||||
$text = 'create: ';
|
||||
}
|
||||
|
||||
$priceList
|
||||
->setKodoper($item['kodoper'])
|
||||
->setSchname($item['schname'])
|
||||
->setSpecname($item['specname'])
|
||||
->setSpeccode($item['speccode'])
|
||||
->setPriceInfo($item['priceInfo'])
|
||||
->setDiscpercent($item['discpercent'])
|
||||
->setDiscprice($item['discprice'])
|
||||
->setStructname($item['structname'])
|
||||
->setFname($item['fname'])
|
||||
->setFilial($item['filial'])
|
||||
->setComment($item['comment'])
|
||||
->setMediaId($item['mediaId'])
|
||||
->setDateUpdate(new \DateTime())
|
||||
->setGroupId($department->getGroupId());
|
||||
|
||||
$this->entityManager->persist($priceList);
|
||||
$processedCount++;
|
||||
|
||||
// Пакетное сохранение для экономии памяти
|
||||
if ($processedCount % $batchSize === 0) {
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear();
|
||||
gc_collect_cycles();
|
||||
|
||||
if ($debug) {
|
||||
$io->info("Flushed batch of {$batchSize} records. Memory usage: " .
|
||||
round(memory_get_usage() / 1024 / 1024, 2) . "MB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Финальное сохранение для оставшихся записей
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear();
|
||||
|
||||
if ($debug) {
|
||||
$io->info('Sleep: 2 sec');
|
||||
}
|
||||
|
||||
if (empty($this->nosleep)) {
|
||||
sleep(2);
|
||||
}
|
||||
}
|
||||
|
||||
$io->success('Successful. Total processed: ' . $processedCount);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function getPricelist($depnum)
|
||||
{
|
||||
$response = [];
|
||||
$flag = true;
|
||||
$firstrow = 1;
|
||||
$lastrow = 500;
|
||||
|
||||
while ($flag) {
|
||||
try {
|
||||
$result = $this->client->request('GET', 'pricelist/list', [
|
||||
'verify_peer' => false,
|
||||
'verify_host' => false,
|
||||
'timeout' => 60,
|
||||
'base_uri' => $this->widgetApiUrl,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'sovamed_bot'
|
||||
],
|
||||
'query' => [
|
||||
'depnum' => $depnum,
|
||||
'firstrow' => $firstrow,
|
||||
'lastrow' => $lastrow
|
||||
],
|
||||
]);
|
||||
|
||||
$firstrow = $lastrow + 1;
|
||||
$lastrow += 500;
|
||||
|
||||
$resultData = $result->toArray(false); // false чтобы избежать преобразования в объекты
|
||||
|
||||
if (empty($resultData['data'])) {
|
||||
$flag = false;
|
||||
} else {
|
||||
yield $resultData['data']; // Используем генератор для экономии памяти
|
||||
}
|
||||
|
||||
if (empty($this->nosleep)) {
|
||||
sleep(3);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$flag = false;
|
||||
// Логирование ошибки можно добавить при необходимости
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\PriceDepartment;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:priceDep',
|
||||
description: 'Обновление отделений для услуг',
|
||||
)]
|
||||
class UploadPriceDepCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private HttpClientInterface $client,
|
||||
private string $widgetApiUrl,
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void { }
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$response = $this->client->request('GET', '/pricelist/departments', [
|
||||
'verify_peer' => false,
|
||||
'verify_host' => false,
|
||||
'base_uri' => $this->widgetApiUrl,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'sovamed_bot'
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $response->toArray();
|
||||
|
||||
foreach ($response['data'] as $item) {
|
||||
$department = $this->entityManager->getRepository(PriceDepartment::class)
|
||||
->findOneBy([
|
||||
'groupId' => $item['id']
|
||||
]);
|
||||
|
||||
if (is_null($department)) {
|
||||
$department = new PriceDepartment();
|
||||
}
|
||||
|
||||
if (empty($item['viewInWeb'])) {
|
||||
$item['viewInWeb'] = 0;
|
||||
}
|
||||
|
||||
$department
|
||||
->setGroupId($item['id'])
|
||||
->setName($item['name'])
|
||||
->setViewInWeb($item['viewInWeb'])
|
||||
->setDoctCount($item['schCount'])
|
||||
;
|
||||
|
||||
$this->entityManager->persist($department);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$io->success('load: '. $department->getId());
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\PromoCrudService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:promo',
|
||||
description: 'Обновление таблицы promo из view_promo'
|
||||
)]
|
||||
final class UploadPromoCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private PromoCrudService $promoCrudService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('view', null, InputOption::VALUE_OPTIONAL, 'SQL view name', 'public.view_promo');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$viewName = $input->getOption('view');
|
||||
if (empty($viewName)) {
|
||||
$viewName = 'public.view_promo';
|
||||
}
|
||||
$viewName = (string) $viewName;
|
||||
|
||||
$io->title('Promo: sync from view_promo');
|
||||
|
||||
try {
|
||||
$this->logger->info('Promo sync start', ['view' => $viewName]);
|
||||
$affected = $this->promoCrudService->syncFromViewPromo($viewName);
|
||||
$io->success('Sync finished. Affected rows: ' . $affected);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Promo sync failed', [
|
||||
'view' => $viewName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$io->error('Ошибка: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\SiteServiceCrudService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'upload:site-services',
|
||||
description: 'Обновление таблицы site_services из public.view_services'
|
||||
)]
|
||||
final class UploadSiteServicesCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private SiteServiceCrudService $siteServiceCrudService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('view', null, InputOption::VALUE_OPTIONAL, 'SQL view name', 'public.view_services');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$viewName = $input->getOption('view');
|
||||
if (empty($viewName)) {
|
||||
$viewName = 'public.view_services';
|
||||
}
|
||||
$viewName = (string) $viewName;
|
||||
|
||||
$io->title('Site services: sync from view_services');
|
||||
|
||||
try {
|
||||
$this->logger->info('Site services sync start', ['view' => $viewName]);
|
||||
$affected = $this->siteServiceCrudService->syncFromViewServices($viewName);
|
||||
$io->success('Sync finished. Affected rows: ' . $affected);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Site services sync failed', [
|
||||
'view' => $viewName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$io->error('Ошибка: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Dto\Content\ContentFilterDto;
|
||||
use App\Entity\Article;
|
||||
use App\Repository\ArticleRepository;
|
||||
use App\Service\Crud\CrudResponder;
|
||||
use App\Service\Pagination\Paginator;
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/article')]
|
||||
final class ArticleController extends AbstractController
|
||||
{
|
||||
private const READ_GROUPS = ['article:read'];
|
||||
private const WRITE_GROUPS = ['article:write'];
|
||||
|
||||
public function __construct(
|
||||
private readonly CrudResponder $crud,
|
||||
private readonly Paginator $paginator,
|
||||
) {
|
||||
}
|
||||
|
||||
#[OA\Tag(name: 'Статьи')]
|
||||
#[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'limit', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))]
|
||||
#[OA\Parameter(name: 'alias', in: 'query', schema: new OA\Schema(type: 'string'))]
|
||||
#[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))]
|
||||
#[Route('/list', name: 'article_list', methods: ['GET'])]
|
||||
public function list(Request $request, ArticleRepository $repository): JsonResponse
|
||||
{
|
||||
$qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request));
|
||||
|
||||
return $this->json($this->paginator->paginateWithLegacyMeta($qb, $request), Response::HTTP_OK, [], [
|
||||
'groups' => self::READ_GROUPS,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/alias/{alias}', name: 'article_show_by_alias', methods: ['GET'])]
|
||||
public function showByAlias(string $alias, ArticleRepository $repository): JsonResponse
|
||||
{
|
||||
$article = $repository->findOneByAlias($alias);
|
||||
if (!$article) {
|
||||
throw $this->createNotFoundException('Статья не найдена');
|
||||
}
|
||||
|
||||
return $this->crud->read($article, self::READ_GROUPS);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'article_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
||||
public function show(Article $article): JsonResponse
|
||||
{
|
||||
return $this->crud->read($article, self::READ_GROUPS);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Article::class, groups: self::WRITE_GROUPS)))]
|
||||
#[Route('/create', name: 'article_create', methods: ['POST'])]
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
return $this->crud->create($request, Article::class, self::WRITE_GROUPS, self::READ_GROUPS);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Article::class, groups: self::WRITE_GROUPS)))]
|
||||
#[Route('/{id}', name: 'article_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
||||
public function update(Request $request, Article $article): JsonResponse
|
||||
{
|
||||
return $this->crud->update($request, $article, self::WRITE_GROUPS, self::READ_GROUPS);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/{id}', name: 'article_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
||||
public function delete(Article $article): JsonResponse
|
||||
{
|
||||
return $this->crud->delete($article);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use App\Service\Client\Interfaces\CalltouchClientServiceInterface;
|
||||
use App\Dto\CalltouchCreateRequestDto;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/calltouch')]
|
||||
final class CalltouchController extends AbstractController
|
||||
{
|
||||
public function __construct (
|
||||
private CalltouchClientServiceInterface $calltouchClientService,
|
||||
private ValidatorInterface $validator,
|
||||
private SerializerInterface $serializer
|
||||
) {}
|
||||
|
||||
#[Route('/create-lead', name: 'app_calltouch_create-lead', methods: ['POST'])]
|
||||
public function createLead(CalltouchCreateRequestDto $dto, Request $request): JsonResponse
|
||||
{
|
||||
$requestData = $request->request->all();
|
||||
|
||||
$errors = $this->validator->validate($dto);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
return $this->json(['errors' => (string) $errors], 400);
|
||||
}
|
||||
|
||||
// $response = $this->calltouchClientService->requestCreate($dto);
|
||||
|
||||
return $this->json([
|
||||
'request' => $requestData,
|
||||
// 'data' => $response,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class DefaultController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_default_comingsoon', methods: ['GET'])]
|
||||
public function comingsoon(): Response
|
||||
{
|
||||
return $this->render('service/comingsoon.html.twig', [
|
||||
'title' => 'Account disabled by server administrator',
|
||||
'message' => 'Account disabled by server administrator',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Department;
|
||||
use App\Repository\DepartmentRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[Route('/department')]
|
||||
final class DepartmentController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private ValidatorInterface $validator,
|
||||
private SerializerInterface $serializer
|
||||
) { }
|
||||
|
||||
#[OA\Tag(name: 'Список отделений')]
|
||||
#[Route('/list', name: 'department_list', methods: ['GET'])]
|
||||
public function list(DepartmentRepository $repository): JsonResponse
|
||||
{
|
||||
$departmentList = $repository->activeAll();
|
||||
|
||||
return $this->json([
|
||||
'data' => $departmentList
|
||||
], Response::HTTP_OK, [], [
|
||||
'groups' => ['department:read']
|
||||
]);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route(
|
||||
path: '/{did}',
|
||||
name: 'department_update',
|
||||
methods: ['PUT'],
|
||||
requirements: ['did' => '\d+']
|
||||
)]
|
||||
public function update(int $did, Request $request, DepartmentRepository $repository): JsonResponse
|
||||
{
|
||||
$department = $repository->findOneBy(['did' => $did]);
|
||||
|
||||
if (!$department) {
|
||||
return new JsonResponse([
|
||||
'message' => 'Department not found'
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$this->serializer->deserialize(
|
||||
$request->getContent(),
|
||||
Department::class,
|
||||
'json',
|
||||
[
|
||||
'groups' => ['department:write'],
|
||||
'object_to_populate' => $department
|
||||
]
|
||||
);
|
||||
|
||||
$errors = $this->validator->validate($department);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
return $this->json($errors, Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $this->json($department, Response::HTTP_OK, [], [
|
||||
'groups' => ['department:read']
|
||||
]);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/create', name: 'department_create', methods: ['POST'])]
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
$department = $this->serializer->deserialize(
|
||||
$request->getContent(),
|
||||
Department::class,
|
||||
'json',
|
||||
['groups' => ['department:write']]
|
||||
);
|
||||
|
||||
$errors = $this->validator->validate($department);
|
||||
if (count($errors) > 0) {
|
||||
return $this->json($errors, Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->em->persist($department);
|
||||
$this->em->flush();
|
||||
|
||||
return $this->json($department, Response::HTTP_CREATED, [], [
|
||||
'groups' => ['department:read']
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Dto\Content\ContentFilterDto;
|
||||
use App\Entity\Disease;
|
||||
use App\Repository\DiseaseRepository;
|
||||
use App\Service\Crud\CrudResponder;
|
||||
use App\Service\Pagination\Paginator;
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/disease')]
|
||||
final class DiseaseController extends AbstractController
|
||||
{
|
||||
private const READ_GROUPS = ['disease:read'];
|
||||
private const WRITE_GROUPS = ['disease:write'];
|
||||
|
||||
public function __construct(
|
||||
private readonly CrudResponder $crud,
|
||||
private readonly Paginator $paginator,
|
||||
) {
|
||||
}
|
||||
|
||||
#[OA\Tag(name: 'Заболевания')]
|
||||
#[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'perPage', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))]
|
||||
#[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))]
|
||||
#[Route('/list', name: 'disease_list', methods: ['GET'])]
|
||||
public function list(Request $request, DiseaseRepository $repository): JsonResponse
|
||||
{
|
||||
$qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request));
|
||||
|
||||
return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [
|
||||
'groups' => self::READ_GROUPS,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'disease_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
||||
public function show(Disease $disease): JsonResponse
|
||||
{
|
||||
return $this->crud->read($disease, self::READ_GROUPS);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Disease::class, groups: self::WRITE_GROUPS)))]
|
||||
#[Route('/create', name: 'disease_create', methods: ['POST'])]
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
return $this->crud->create($request, Disease::class, self::WRITE_GROUPS, self::READ_GROUPS);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Disease::class, groups: self::WRITE_GROUPS)))]
|
||||
#[Route('/{id}', name: 'disease_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
||||
public function update(Request $request, Disease $disease): JsonResponse
|
||||
{
|
||||
return $this->crud->update($request, $disease, self::WRITE_GROUPS, self::READ_GROUPS);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/{id}', name: 'disease_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
||||
public function delete(Disease $disease): JsonResponse
|
||||
{
|
||||
return $this->crud->delete($disease);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Filial;
|
||||
use App\Repository\FilialRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use App\Service\FileUploader\Interfaces\FileUploaderServiceInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use App\Service\Image\Interfaces\ImageServiceInterface;
|
||||
use App\Dto\FileUploadDto;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Exception;
|
||||
|
||||
#[Route('/filial')]
|
||||
final class FilialController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private ValidatorInterface $validator,
|
||||
private SerializerInterface $serializer,
|
||||
private ImageServiceInterface $imageService
|
||||
) { }
|
||||
|
||||
#[OA\Tag(name: 'Список филиалов')]
|
||||
#[Route(path: '/list', name: 'filial_list', methods: ['GET'])]
|
||||
public function list(Request $request, FilialRepository $repository): JsonResponse
|
||||
{
|
||||
$regionId = $request->query->getInt('regionId', 0);
|
||||
$criteria = ['active' => true];
|
||||
if ($regionId > 0) {
|
||||
$criteria['regionId'] = $regionId;
|
||||
}
|
||||
$filials = $repository->findBy($criteria);
|
||||
|
||||
return $this->json(['data' => $filials],
|
||||
Response::HTTP_OK, [], [
|
||||
'groups' => ['filial:read']
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/by-region/{regionId}',
|
||||
name: 'filial_by_region',
|
||||
methods: ['GET'],
|
||||
requirements: ['regionId' => '\d+']
|
||||
)]
|
||||
public function byRegion(int $regionId, FilialRepository $repository): JsonResponse
|
||||
{
|
||||
$filials = $repository->findBy([
|
||||
'regionId' => $regionId,
|
||||
'active' => true
|
||||
]);
|
||||
|
||||
return $this->json(['data' => $filials]
|
||||
, Response::HTTP_OK, [], [
|
||||
'groups' => ['filial:read']
|
||||
]);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route(
|
||||
path: '/{fid}',
|
||||
name: 'filial_update',
|
||||
methods: ['PUT'],
|
||||
requirements: ['fid' => '\d+']
|
||||
)]
|
||||
public function update(int $fid, Request $request, FilialRepository $repository): JsonResponse
|
||||
{
|
||||
$filial = $repository->findOneBy(['fid' => $fid]);
|
||||
|
||||
if (!$filial) {
|
||||
return new JsonResponse([
|
||||
'message' => 'Filial not found'
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$this->serializer->deserialize(
|
||||
$request->getContent(),
|
||||
Filial::class,
|
||||
'json',
|
||||
[
|
||||
'groups' => ['filial:write'],
|
||||
'object_to_populate' => $filial
|
||||
]
|
||||
);
|
||||
|
||||
$errors = $this->validator->validate($filial);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
return $this->json($errors, Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $this->json($filial, Response::HTTP_OK, [], [
|
||||
'groups' => ['filial:read']
|
||||
]);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/create', name: 'filial_create', methods: ['POST'])]
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
$filial = $this->serializer->deserialize(
|
||||
$request->getContent(),
|
||||
Filial::class,
|
||||
'json',
|
||||
['groups' => ['filial:write']]
|
||||
);
|
||||
|
||||
$errors = $this->validator->validate($filial);
|
||||
if (count($errors) > 0) {
|
||||
return $this->json($errors, Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->em->persist($filial);
|
||||
$this->em->flush();
|
||||
|
||||
return $this->json($filial, Response::HTTP_CREATED, [], [
|
||||
'groups' => ['filial:read']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/picture/{id}', name: 'filial_upload_picture', methods: ['POST'], requirements: ['id' => '\d+'])]
|
||||
public function pictureUpload(
|
||||
Filial $filial,
|
||||
Request $request,
|
||||
FileUploadDto $dto,
|
||||
FileUploaderServiceInterface $fileUploader
|
||||
): JsonResponse {
|
||||
try {
|
||||
$uploadedFile = $request->files->get('picture');
|
||||
|
||||
if (!$uploadedFile instanceof UploadedFile) {
|
||||
return $this->json(['error' => 'File not uploaded'], 400);
|
||||
}
|
||||
|
||||
$dto->file = $uploadedFile;
|
||||
$errors = $this->validator->validate($dto);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
return $this->json($errors, 400);
|
||||
}
|
||||
|
||||
$fileUploader->remove($fileUploader->getTargetDirectory() .'/'. $filial->getPicture());
|
||||
$fileUploader->setTargetDirectory('filial');
|
||||
$fileName = $fileUploader->upload($uploadedFile);
|
||||
|
||||
$filial->setPicture('filial/'. $fileName);
|
||||
$this->em->persist($filial);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
return $this->json($filial, 200, [], [
|
||||
'groups' => ['filial:read']
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return $this->json([
|
||||
'error' => 'File upload failed',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/picture/{id}', name: 'filial_picture', methods: ['GET'])]
|
||||
public function filialPicture(Filial $filial, Request $request): Response
|
||||
{
|
||||
$height = min($request->query->getInt('height', 300), 800);
|
||||
$width = min($request->query->getInt('width', 300), 600);
|
||||
|
||||
$uploadDir = $this->getParameter('upload_directory');
|
||||
|
||||
return $this->imageService->getPicture($uploadDir . '/'. $filial->getPicture(), $width, $height);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user