Compare commits
1 Commits
master
...
feature/do
Author | SHA1 | Date | |
---|---|---|---|
101de8916e |
34
.dockerignore
Normal file
34
.dockerignore
Normal file
@ -0,0 +1,34 @@
|
||||
**/*.log
|
||||
**/*.md
|
||||
**/*.php~
|
||||
**/*.dist.php
|
||||
**/*.dist
|
||||
**/*.cache
|
||||
**/._*
|
||||
**/.dockerignore
|
||||
**/.DS_Store
|
||||
**/.git/
|
||||
**/.gitattributes
|
||||
**/.gitignore
|
||||
**/.gitmodules
|
||||
**/compose.*.yaml
|
||||
**/compose.*.yml
|
||||
**/compose.yaml
|
||||
**/compose.yml
|
||||
**/docker-compose.*.yaml
|
||||
**/docker-compose.*.yml
|
||||
**/docker-compose.yaml
|
||||
**/docker-compose.yml
|
||||
**/Dockerfile
|
||||
**/Thumbs.db
|
||||
.github/
|
||||
docs/
|
||||
public/bundles/
|
||||
tests/
|
||||
var/
|
||||
vendor/
|
||||
.editorconfig
|
||||
.env.*.local
|
||||
.env.local
|
||||
.env.local.php
|
||||
.env.test
|
@ -1,17 +0,0 @@
|
||||
# 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
|
2
.env
2
.env
@ -24,6 +24,6 @@ APP_SECRET=
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
#
|
||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.13-MariaDB&charset=utf8mb4"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=mariadb-10.9.5&charset=utf8mb4"
|
||||
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
76
.github/workflows/ci.yml
vendored
Normal file
76
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: ~
|
||||
workflow_dispatch: ~
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build Docker images
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
pull: true
|
||||
load: true
|
||||
files: |
|
||||
compose.yaml
|
||||
compose.override.yaml
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=${{github.ref}}
|
||||
*.cache-from=type=gha,scope=refs/heads/main
|
||||
*.cache-to=type=gha,scope=${{github.ref}},mode=max
|
||||
-
|
||||
name: Start services
|
||||
run: docker compose up --wait --no-build
|
||||
-
|
||||
name: Check HTTP reachability
|
||||
run: curl -v --fail-with-body http://localhost
|
||||
-
|
||||
name: Check HTTPS reachability
|
||||
if: false # Remove this line when the homepage will be configured, or change the path to check
|
||||
run: curl -vk --fail-with-body https://localhost
|
||||
-
|
||||
name: Check Mercure reachability
|
||||
run: curl -vkI --fail-with-body https://localhost/.well-known/mercure?topic=test
|
||||
-
|
||||
name: Create test database
|
||||
if: false # Remove this line if Doctrine ORM is installed
|
||||
run: docker compose exec -T php bin/console -e test doctrine:database:create
|
||||
-
|
||||
name: Run migrations
|
||||
if: false # Remove this line if Doctrine Migrations is installed
|
||||
run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction
|
||||
-
|
||||
name: Run PHPUnit
|
||||
if: false # Remove this line if PHPUnit is installed
|
||||
run: docker compose exec -T php bin/phpunit
|
||||
-
|
||||
name: Doctrine Schema Validator
|
||||
if: false # Remove this line if Doctrine ORM is installed
|
||||
run: docker compose exec -T php bin/console -e test doctrine:schema:validate
|
||||
lint:
|
||||
name: Docker Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Lint Dockerfile
|
||||
uses: hadolint/hadolint-action@v3.1.0
|
77
Dockerfile
Normal file
77
Dockerfile
Normal file
@ -0,0 +1,77 @@
|
||||
#syntax=docker/dockerfile:1
|
||||
|
||||
# Versions
|
||||
FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream
|
||||
|
||||
# The different stages of this Dockerfile are meant to be built into separate images
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
|
||||
# https://docs.docker.com/compose/compose-file/#target
|
||||
|
||||
|
||||
# Base FrankenPHP image
|
||||
FROM frankenphp_upstream AS frankenphp_base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
VOLUME /app/var/
|
||||
|
||||
# persistent / runtime deps
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
acl \
|
||||
file \
|
||||
gettext \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
@composer \
|
||||
apcu \
|
||||
intl \
|
||||
opcache \
|
||||
zip \
|
||||
;
|
||||
|
||||
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
# Transport to use by Mercure (default to Bolt)
|
||||
ENV MERCURE_TRANSPORT_URL=bolt:///data/mercure.db
|
||||
|
||||
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
|
||||
|
||||
###> recipes ###
|
||||
###> doctrine/doctrine-bundle ###
|
||||
#RUN install-php-extensions pdo pdo_mysql
|
||||
RUN docker-php-ext-install pdo pdo_mysql
|
||||
RUN docker-php-ext-enable pdo pdo_mysql
|
||||
###< doctrine/doctrine-bundle ###
|
||||
###< recipes ###
|
||||
|
||||
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
|
||||
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||
COPY --link frankenphp/Caddyfile /etc/frankenphp/Caddyfile
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint"]
|
||||
|
||||
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
||||
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile" ]
|
||||
|
||||
# Dev FrankenPHP image
|
||||
FROM frankenphp_base AS frankenphp_dev
|
||||
|
||||
ENV APP_ENV=dev
|
||||
ENV XDEBUG_MODE=off
|
||||
#ENV FRANKENPHP_WORKER_CONFIG=watch
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
xdebug \
|
||||
;
|
||||
|
||||
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
|
||||
|
||||
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--watch" ]
|
32
compose.override.yaml
Normal file
32
compose.override.yaml
Normal file
@ -0,0 +1,32 @@
|
||||
# Development environment override
|
||||
services:
|
||||
php:
|
||||
build:
|
||||
context: .
|
||||
target: frankenphp_dev
|
||||
volumes:
|
||||
- ./:/app
|
||||
- ./frankenphp/Caddyfile:/etc/frankenphp/Caddyfile:ro
|
||||
- ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/app.conf.d/20-app.dev.ini:ro
|
||||
# If you develop on Mac or Windows you can remove the vendor/ directory
|
||||
# from the bind-mount for better performance by enabling the next line:
|
||||
#- /app/vendor
|
||||
environment:
|
||||
#FRANKENPHP_WORKER_CONFIG: watch
|
||||
MERCURE_EXTRA_DIRECTIVES: demo
|
||||
# See https://xdebug.org/docs/all_settings#mode
|
||||
XDEBUG_MODE: "${XDEBUG_MODE:-off}"
|
||||
APP_ENV: "${APP_ENV:-dev}"
|
||||
extra_hosts:
|
||||
# Ensure that host.docker.internal is correctly defined on Linux
|
||||
- host.docker.internal:host-gateway
|
||||
tty: true
|
||||
|
||||
###> symfony/mercure-bundle ###
|
||||
###< symfony/mercure-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
ports:
|
||||
- "3306"
|
||||
###< doctrine/doctrine-bundle ###
|
10
compose.prod.yaml
Normal file
10
compose.prod.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
# Production environment override
|
||||
services:
|
||||
php:
|
||||
build:
|
||||
context: .
|
||||
target: frankenphp_prod
|
||||
environment:
|
||||
APP_SECRET: ${APP_SECRET}
|
||||
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
|
69
compose.yaml
Normal file
69
compose.yaml
Normal file
@ -0,0 +1,69 @@
|
||||
version: "3"
|
||||
services:
|
||||
php:
|
||||
image: ${IMAGES_PREFIX:-}app-php
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- database
|
||||
environment:
|
||||
SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
|
||||
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
|
||||
#DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
|
||||
DATABASE_URL: mysql://root:password@database:3306/snips?serverVersion=11.7.2-MariaDB
|
||||
# Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
|
||||
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
|
||||
MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}:${HTTPS_PORT:-443}/.well-known/mercure}
|
||||
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||
# The two next lines can be removed after initial installation
|
||||
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
|
||||
STABILITY: ${STABILITY:-stable}
|
||||
volumes:
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
ports:
|
||||
# HTTP
|
||||
- target: 80
|
||||
published: ${HTTP_PORT:-80}
|
||||
protocol: tcp
|
||||
# HTTPS
|
||||
- target: 443
|
||||
published: ${HTTPS_PORT:-443}
|
||||
protocol: tcp
|
||||
# HTTP/3
|
||||
- target: 443
|
||||
published: ${HTTP3_PORT:-443}
|
||||
protocol: udp
|
||||
|
||||
# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service
|
||||
###> symfony/mercure-bundle ###
|
||||
###< symfony/mercure-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
image: 'mariadb:latest'
|
||||
environment:
|
||||
MARIADB_USER: app
|
||||
MARIADB_PASSWORD: password
|
||||
MARIADB_DATABASE: snips
|
||||
MARIADB_ROOT_PASSWORD: password
|
||||
ports:
|
||||
# To allow the host machine to access the ports below, modify the lines below.
|
||||
# For example, to allow the host to connect to port 3306 on the container, you would change
|
||||
# "3306" to "3306:3306". Where the first port is exposed to the host and the second is the container port.
|
||||
# See https://docs.docker.com/compose/compose-file/compose-file-v3/#ports for more information.
|
||||
- '3306'
|
||||
volumes:
|
||||
- database_data:/var/lib/mysql
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
###> symfony/mercure-bundle ###
|
||||
###< symfony/mercure-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database_data:
|
||||
###< doctrine/doctrine-bundle ###
|
@ -8,20 +8,18 @@
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"doctrine/doctrine-bundle": "^2.9",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||
"doctrine/orm": "^3.4",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.2",
|
||||
"doctrine/orm": "^2.14",
|
||||
"league/commonmark": "^2.6",
|
||||
"league/pipeline": "^1.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.1",
|
||||
"symfony/asset": "*",
|
||||
"symfony/console": "*",
|
||||
"symfony/dotenv": "*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "*",
|
||||
"symfony/framework-bundle": "*",
|
||||
"symfony/monolog-bundle": "^3.0",
|
||||
"symfony/object-mapper": "7.3.*",
|
||||
"symfony/property-access": "*",
|
||||
"symfony/property-info": "*",
|
||||
"symfony/runtime": "*",
|
||||
@ -31,7 +29,6 @@
|
||||
"symfony/uid": "*",
|
||||
"symfony/validator": "*",
|
||||
"symfony/yaml": "*",
|
||||
"tempest/highlight": "^2.11",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^3.0"
|
||||
},
|
||||
@ -85,7 +82,8 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.3.*"
|
||||
"require": "7.2.*",
|
||||
"docker": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1200
composer.lock
generated
1200
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@ doctrine:
|
||||
orm:
|
||||
report_fields_where_declared: true
|
||||
auto_generate_proxy_classes: true
|
||||
enable_native_lazy_objects: true
|
||||
enable_lazy_ghost_objects: true
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
identity_generation_preferences:
|
||||
|
@ -1,3 +0,0 @@
|
||||
framework:
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
@ -8,6 +8,4 @@ when@dev:
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
profiler:
|
||||
collect: false
|
||||
collect_serializer_data: true
|
||||
profiler: { collect: false }
|
||||
|
@ -1,4 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
||||
|
@ -1,8 +1,8 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
||||
|
@ -15,6 +15,7 @@ services:
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
exclude:
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
57
frankenphp/Caddyfile
Normal file
57
frankenphp/Caddyfile
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
{$CADDY_GLOBAL_OPTIONS}
|
||||
|
||||
frankenphp {
|
||||
{$FRANKENPHP_CONFIG}
|
||||
}
|
||||
}
|
||||
|
||||
{$CADDY_EXTRA_CONFIG}
|
||||
|
||||
{$SERVER_NAME:localhost} {
|
||||
log {
|
||||
{$CADDY_SERVER_LOG_OPTIONS}
|
||||
# Redact the authorization query parameter that can be set by Mercure
|
||||
format filter {
|
||||
request>uri query {
|
||||
replace authorization REDACTED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root /app/public
|
||||
encode zstd br gzip
|
||||
|
||||
mercure {
|
||||
# Publisher JWT key
|
||||
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
|
||||
# Subscriber JWT key
|
||||
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
|
||||
# Allow anonymous subscribers (double-check that it's what you want)
|
||||
anonymous
|
||||
# Enable the subscription API (double-check that it's what you want)
|
||||
subscriptions
|
||||
# Extra directives
|
||||
{$MERCURE_EXTRA_DIRECTIVES}
|
||||
}
|
||||
|
||||
vulcain
|
||||
|
||||
{$CADDY_SERVER_EXTRA_DIRECTIVES}
|
||||
|
||||
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
|
||||
header ?Permissions-Policy "browsing-topics=()"
|
||||
|
||||
@phpRoute {
|
||||
not path /.well-known/mercure*
|
||||
not file {path}
|
||||
}
|
||||
rewrite @phpRoute index.php
|
||||
|
||||
@frontController path index.php
|
||||
php @frontController
|
||||
|
||||
file_server {
|
||||
hide *.php
|
||||
}
|
||||
}
|
13
frankenphp/conf.d/10-app.ini
Normal file
13
frankenphp/conf.d/10-app.ini
Normal file
@ -0,0 +1,13 @@
|
||||
expose_php = 0
|
||||
date.timezone = UTC
|
||||
apc.enable_cli = 1
|
||||
session.use_strict_mode = 1
|
||||
zend.detect_unicode = 0
|
||||
|
||||
; https://symfony.com/doc/current/performance.html
|
||||
realpath_cache_size = 4096K
|
||||
realpath_cache_ttl = 600
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.max_accelerated_files = 20000
|
||||
opcache.memory_consumption = 256
|
||||
opcache.enable_file_override = 1
|
5
frankenphp/conf.d/20-app.dev.ini
Normal file
5
frankenphp/conf.d/20-app.dev.ini
Normal file
@ -0,0 +1,5 @@
|
||||
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
|
||||
; See https://github.com/docker/for-linux/issues/264
|
||||
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
|
||||
; Add `start_with_request=yes` to start debug session on each request
|
||||
xdebug.client_host = host.docker.internal
|
5
frankenphp/conf.d/20-app.prod.ini
Normal file
5
frankenphp/conf.d/20-app.prod.ini
Normal file
@ -0,0 +1,5 @@
|
||||
; https://symfony.com/doc/current/performance.html#use-the-opcache-class-preloading
|
||||
opcache.preload_user = root
|
||||
opcache.preload = /app/config/preload.php
|
||||
; https://symfony.com/doc/current/performance.html#don-t-check-php-files-timestamps
|
||||
opcache.validate_timestamps = 0
|
67
frankenphp/docker-entrypoint.sh
Executable file
67
frankenphp/docker-entrypoint.sh
Executable file
@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
||||
# Install the project the first time PHP is started
|
||||
# After the installation, the following block can be deleted
|
||||
if [ ! -f composer.json ]; then
|
||||
rm -Rf tmp/
|
||||
composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install
|
||||
|
||||
cd tmp
|
||||
cp -Rp . ..
|
||||
cd -
|
||||
rm -Rf tmp/
|
||||
|
||||
composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony
|
||||
composer config --json extra.symfony.docker 'true'
|
||||
|
||||
if grep -q ^DATABASE_URL= .env; then
|
||||
echo 'To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build --wait'
|
||||
sleep infinity
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
|
||||
composer install --prefer-dist --no-progress --no-interaction
|
||||
fi
|
||||
|
||||
# Display information about the current project
|
||||
# Or about an error in project initialization
|
||||
php bin/console -V
|
||||
|
||||
if grep -q ^DATABASE_URL= .env.local; then
|
||||
echo 'Waiting for database to be ready...'
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
||||
if [ $? -eq 255 ]; then
|
||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
||||
done
|
||||
|
||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
||||
echo 'The database is not up or not reachable:'
|
||||
echo "$DATABASE_ERROR"
|
||||
exit 1
|
||||
else
|
||||
echo 'The database is now ready and reachable'
|
||||
fi
|
||||
|
||||
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
|
||||
echo 'Migrating database...'
|
||||
php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing
|
||||
fi
|
||||
fi
|
||||
|
||||
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||
|
||||
echo 'PHP app ready!'
|
||||
fi
|
||||
|
||||
exec docker-php-entrypoint "$@"
|
@ -1,87 +0,0 @@
|
||||
pre, code {
|
||||
color: #1f2328;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.hl-keyword {
|
||||
color: #cf222e;
|
||||
}
|
||||
|
||||
.hl-property {
|
||||
color: #8250df;
|
||||
}
|
||||
|
||||
.hl-attribute {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hl-type {
|
||||
color: #EA4334;
|
||||
}
|
||||
|
||||
.hl-generic {
|
||||
color: #9d3af6;
|
||||
}
|
||||
|
||||
.hl-value {
|
||||
color: #0a3069;
|
||||
}
|
||||
|
||||
.hl-literal {
|
||||
color: #0a3069;
|
||||
}
|
||||
|
||||
.hl-number {
|
||||
color: #0a3069;
|
||||
}
|
||||
|
||||
.hl-variable {
|
||||
color: #953800;
|
||||
}
|
||||
|
||||
.hl-comment {
|
||||
color: #6e7781;
|
||||
}
|
||||
|
||||
.hl-blur {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.hl-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hl-em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hl-addition {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
background-color: #00FF0022;
|
||||
}
|
||||
|
||||
.hl-deletion {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
background-color: #FF000011;
|
||||
}
|
||||
|
||||
.hl-gutter {
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
padding: 0 1ch;
|
||||
margin-right: 1ch;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.hl-gutter-addition {
|
||||
background-color: #34A853;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hl-gutter-deletion {
|
||||
background-color: #EA4334;
|
||||
color: #fff;
|
||||
}
|
@ -26,16 +26,17 @@ class SnipUpdateContentCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
// $io = new SymfonyStyle($input, $output);
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$qb = $this->snipContentRepository->createQueryBuilder('s');
|
||||
$qb->where('s.text IS NOT NULL');
|
||||
|
||||
$c = 0;
|
||||
/** @var SnipContent $snipContent */
|
||||
foreach ($qb->getQuery()->getResult() as $snipContent) {
|
||||
$text = $snipContent->text;
|
||||
$text = $snipContent->getText();
|
||||
$text = Lexer::reconstruct(Lexer::tokenize($text));
|
||||
$snipContent->text = $text;
|
||||
$snipContent->setText($text);
|
||||
$this->snipContentRepository->save($snipContent);
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,6 @@ use App\Security\Voter\SnipVoter;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
||||
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class ApiController extends AbstractApiController
|
||||
@ -35,11 +34,11 @@ class ApiController extends AbstractApiController
|
||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
|
||||
|
||||
return $this->successResponse([
|
||||
'id' => $snip->id,
|
||||
'id' => $snip->getId(),
|
||||
'content' => $snip->getActiveText(),
|
||||
'createdBy' => [
|
||||
'id' => $snip->createdBy->getId(),
|
||||
'name' => $snip->createdBy->getName(),
|
||||
'id' => $snip->getCreatedBy()->getId(),
|
||||
'name' => $snip->getCreatedBy()->getName(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
@ -50,29 +49,28 @@ class ApiController extends AbstractApiController
|
||||
#[MapRequestPayload] SnipPostRequest $request,
|
||||
SnipContentService $cs,
|
||||
SnipRepository $repo,
|
||||
ObjectMapperInterface $mapper,
|
||||
): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||
|
||||
if (!($snip->activeVersion === $snip->getLatestVersion())) {
|
||||
if (!($snip->getActiveVersion() === $snip->getLatestVersion())) {
|
||||
return $this->errorResponse('Snip is not the latest version');
|
||||
}
|
||||
|
||||
$mapper->map($request, $snip);
|
||||
$request->pushToSnip($snip);
|
||||
$repo->save($snip);
|
||||
if ($request->content !== null) {
|
||||
$cs->update($snip, $request->content, $request->contentName);
|
||||
$cs->update($snip, $request->content);
|
||||
}
|
||||
|
||||
return $this->successResponse([
|
||||
'id' => $snip->id,
|
||||
'name' => $snip->name,
|
||||
'id' => $snip->getId(),
|
||||
'name' => $snip->getName(),
|
||||
'content' => $snip->getActiveText(),
|
||||
'createdBy' => [
|
||||
'id' => $snip->createdBy->getId(),
|
||||
'name' => $snip->createdBy->getName(),
|
||||
'id' => $snip->getCreatedBy()->getId(),
|
||||
'name' => $snip->getCreatedBy()->getName(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,4 +17,4 @@ class HomeController extends AbstractController
|
||||
return $this->redirectToRoute('snip_public');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,10 +18,10 @@ class SnipContentController extends AbstractController
|
||||
#[Route('/compare/{to}/{from}', name: '_compare')]
|
||||
public function compare(SnipContent $to, ?SnipContent $from = null): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $to->snip);
|
||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $to->getSnip());
|
||||
|
||||
if ($from === null) {
|
||||
$from = $to->parent;
|
||||
$from = $to->getParent();
|
||||
}
|
||||
|
||||
$diff = MyersDiff::buildDiffLines(
|
||||
@ -30,8 +30,8 @@ class SnipContentController extends AbstractController
|
||||
);
|
||||
|
||||
return $this->render('content/compare.html.twig', [
|
||||
'snip' => $to->snip,
|
||||
'snip' => $to->getSnip(),
|
||||
'diff' => $diff,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@ -85,20 +85,22 @@ class SnipController extends AbstractController
|
||||
* Temporary solution to prevent editing of old versions
|
||||
* It technically fully works, but rendering the version history needs an update first
|
||||
*/
|
||||
$isLatest = $snip->activeVersion === $snip->getLatestVersion();
|
||||
$isLatest = $snip->getActiveVersion() === $snip->getLatestVersion();
|
||||
if (!$isLatest) {
|
||||
$this->addFlash('error', 'Snip is not the latest version, changes will not be saved.');
|
||||
}
|
||||
|
||||
$form = $this->createForm(SnipType::class, $snip)
|
||||
->add('Save', SubmitType::class);
|
||||
$form->get('content')->setData($snip->getActiveText());
|
||||
$form = $this->createForm(SnipType::class, $snip);
|
||||
$form->add('Save', SubmitType::class);
|
||||
if ($snip->getId()) {
|
||||
$form->get('content')->setData($snip->getActiveText());
|
||||
}
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
if (!$isLatest) {
|
||||
return $this->redirectToRoute('snip_single', [
|
||||
'snip' => $snip->id,
|
||||
'snip' => $snip->getId(),
|
||||
]);
|
||||
}
|
||||
$this->repository->save($snip);
|
||||
@ -111,7 +113,7 @@ class SnipController extends AbstractController
|
||||
$this->addFlash('success', sprintf('Snip "%s" saved', $snip));
|
||||
|
||||
return $this->redirectToRoute('snip_single', [
|
||||
'snip' => $snip->id,
|
||||
'snip' => $snip->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -125,32 +127,11 @@ class SnipController extends AbstractController
|
||||
public function new(Request $request, SnipContentService $contentService): Response
|
||||
{
|
||||
$snip = new Snip();
|
||||
$snip->setCreatedAtNow();
|
||||
$snip->createdBy = $this->getUser();
|
||||
$snip->setCreatedAtNow()
|
||||
->setCreatedBy($this->getUser())
|
||||
;
|
||||
|
||||
$form = $this->createForm(SnipType::class, $snip);
|
||||
$form->add('Create', SubmitType::class);
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->repository->save($snip);
|
||||
$contentService->update(
|
||||
$snip,
|
||||
$form->get('content')->getData(),
|
||||
$form->get('contentName')->getData()
|
||||
);
|
||||
|
||||
$this->addFlash('success', sprintf('Snip "%s" created', $snip));
|
||||
|
||||
return $this->redirectToRoute('snip_single', [
|
||||
'snip' => $snip->id,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('snip/create.html.twig', [
|
||||
'snip' => $snip,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
return $this->edit($snip, $request, $contentService);
|
||||
}
|
||||
|
||||
#[Route('/delete/{snip}', name: '_delete')]
|
||||
@ -161,7 +142,7 @@ class SnipController extends AbstractController
|
||||
$form = $this->createForm(ConfirmationType::class);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$snip->activeVersion = null;
|
||||
$snip->setActiveVersion(null);
|
||||
$this->repository->save($snip);
|
||||
$this->repository->remove($snip);
|
||||
$this->addFlash('success', sprintf('Snip "%s" deleted', $snip));
|
||||
@ -178,14 +159,14 @@ class SnipController extends AbstractController
|
||||
public function archive(Snip $snip): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||
$snip->archived = !$snip->archived;
|
||||
$snip->setArchived(!$snip->isArchived());
|
||||
$this->repository->save($snip);
|
||||
if ($snip->archived) {
|
||||
if ($snip->isArchived()) {
|
||||
$this->addFlash('success', sprintf('Snip "%s" archived', $snip));
|
||||
} else {
|
||||
$this->addFlash('success', sprintf('Snip "%s" unarchived', $snip));
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('snip_edit', ['snip' => $snip->id]);
|
||||
return $this->redirectToRoute('snip_edit', ['snip' => $snip->getId()]);
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ class VersionController extends AbstractController
|
||||
public function index(Snip $snip): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||
|
||||
|
||||
return $this->render('version/index.html.twig', [
|
||||
'snip' => $snip,
|
||||
]);
|
||||
@ -33,7 +33,7 @@ class VersionController extends AbstractController
|
||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||
|
||||
$this->contentService->setVersion($snip, $version);
|
||||
$this->addFlash('success', 'Snip version set to ' . $version->id);
|
||||
return $this->redirectToRoute('snip_single', ['snip' => $snip->id]);
|
||||
$this->addFlash('success', 'Snip version set to ' . $version->getId());
|
||||
return $this->redirectToRoute('snip_single', ['snip' => $snip->getId()]);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto\Condition;
|
||||
|
||||
use Symfony\Component\ObjectMapper\ConditionCallableInterface;
|
||||
|
||||
class ConditionNotNull implements ConditionCallableInterface
|
||||
{
|
||||
|
||||
public function __invoke(mixed $value, object $source, ?object $target): bool
|
||||
{
|
||||
return null !== $value;
|
||||
}
|
||||
}
|
@ -20,4 +20,13 @@ readonly class SnipFilterRequest implements CachableDtoInterface
|
||||
public ?string $sort = self::SORT_NAME,
|
||||
public ?string $tag = self::TAG_ALL,
|
||||
) {}
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'visibility' => $this->visibility,
|
||||
'sort' => $this->sort,
|
||||
'tag' => $this->tag,
|
||||
];
|
||||
}
|
||||
}
|
@ -2,21 +2,27 @@
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Dto\Condition\ConditionNotNull;
|
||||
use App\Entity\Snip;
|
||||
use Symfony\Component\ObjectMapper\Attribute\Map;
|
||||
|
||||
#[Map(target: Snip::class)]
|
||||
class SnipPostRequest
|
||||
{
|
||||
public function __construct(
|
||||
#[Map(if: new ConditionNotNull())]
|
||||
public ?string $name = null,
|
||||
public ?string $content = null,
|
||||
#[Map(if: new ConditionNotNull())]
|
||||
public ?bool $public = null,
|
||||
#[Map(if: new ConditionNotNull())]
|
||||
public ?bool $visible = null,
|
||||
public ?string $contentName = null,
|
||||
) {}
|
||||
}
|
||||
|
||||
public function pushToSnip(Snip $snip): void
|
||||
{
|
||||
if ($this->name !== null) {
|
||||
$snip->setName($this->name);
|
||||
}
|
||||
if ($this->public !== null) {
|
||||
$snip->setPublic($this->public);
|
||||
}
|
||||
if ($this->visible !== null) {
|
||||
$snip->setVisible($this->visible);
|
||||
}
|
||||
}
|
||||
}
|
@ -10,23 +10,47 @@ trait TrackedTrait
|
||||
{
|
||||
#[ORM\Column]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
public ?DateTime $createdAt = null;
|
||||
private ?DateTime $createdAt = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
public ?User $createdBy = null;
|
||||
private ?User $createdBy = null;
|
||||
|
||||
public function getCreatedBy(): ?User
|
||||
{
|
||||
return $this->createdBy;
|
||||
}
|
||||
|
||||
public function setCreatedBy(?User $createdBy): self
|
||||
{
|
||||
$this->createdBy = $createdBy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(DateTime $createdAt): self
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCreatedAtNowNoSeconds(): self
|
||||
{
|
||||
$this->createdAt = DateTime::createFromFormat('Y-m-d H:i', date('Y-m-d H:i'));
|
||||
$this->setCreatedAt(DateTime::createFromFormat('Y-m-d H:i', date('Y-m-d H:i')));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCreatedAtNow(): self
|
||||
{
|
||||
$this->createdAt = new DateTime();
|
||||
$this->setCreatedAt(new DateTime());
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
}
|
@ -17,34 +17,34 @@ class Snip
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
public ?int $id = null;
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
public ?string $name = null;
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column]
|
||||
public bool $public = false;
|
||||
private bool $public = false;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'snip', targetEntity: SnipContent::class, orphanRemoval: true)]
|
||||
public Collection $snipContents;
|
||||
private Collection $snipContents;
|
||||
|
||||
#[ORM\OneToOne]
|
||||
public ?SnipContent $activeVersion = null;
|
||||
private ?SnipContent $activeVersion = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
public ?string $parser = null;
|
||||
private ?string $parser = null;
|
||||
|
||||
#[ORM\Column]
|
||||
public bool $visible = true;
|
||||
private bool $visible = true;
|
||||
|
||||
#[ORM\Column]
|
||||
public bool $archived = false;
|
||||
private bool $archived = false;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Tag>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Tag::class, mappedBy: 'snips')]
|
||||
public Collection $tags;
|
||||
private Collection $tags;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@ -59,14 +59,51 @@ class Snip
|
||||
|
||||
public function getActiveText(): string
|
||||
{
|
||||
return SnipContentService::rebuildText($this->activeVersion);
|
||||
return SnipContentService::rebuildText($this->getActiveVersion());
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isPublic(): ?bool
|
||||
{
|
||||
return $this->public;
|
||||
}
|
||||
|
||||
public function setPublic(bool $public): self
|
||||
{
|
||||
$this->public = $public;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, SnipContent>
|
||||
*/
|
||||
public function getSnipContents(): Collection
|
||||
{
|
||||
return $this->snipContents;
|
||||
}
|
||||
|
||||
public function addSnipContent(SnipContent $snipContent): self
|
||||
{
|
||||
if (!$this->snipContents->contains($snipContent)) {
|
||||
$this->snipContents->add($snipContent);
|
||||
$snipContent->snip = $this;
|
||||
$snipContent->setSnip($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -76,8 +113,8 @@ class Snip
|
||||
{
|
||||
if ($this->snipContents->removeElement($snipContent)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($snipContent->snip === $this) {
|
||||
$snipContent->snip = null;
|
||||
if ($snipContent->getSnip() === $this) {
|
||||
$snipContent->setSnip(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +126,62 @@ class Snip
|
||||
return $this->snipContents->last() ?: null;
|
||||
}
|
||||
|
||||
public function getActiveVersion(): ?SnipContent
|
||||
{
|
||||
return $this->activeVersion;
|
||||
}
|
||||
|
||||
public function setActiveVersion(?SnipContent $activeVersion): static
|
||||
{
|
||||
$this->activeVersion = $activeVersion;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParser(): ?string
|
||||
{
|
||||
return $this->parser;
|
||||
}
|
||||
|
||||
public function setParser(string $parser): static
|
||||
{
|
||||
$this->parser = $parser;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVisible(): ?bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
public function setVisible(bool $visible): static
|
||||
{
|
||||
$this->visible = $visible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isArchived(): ?bool
|
||||
{
|
||||
return $this->archived;
|
||||
}
|
||||
|
||||
public function setArchived(bool $archived): static
|
||||
{
|
||||
$this->archived = $archived;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Tag>
|
||||
*/
|
||||
public function getTags(): Collection
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function addTag(Tag $tag): static
|
||||
{
|
||||
if (!$this->tags->contains($tag)) {
|
||||
|
@ -17,30 +17,124 @@ class SnipContent
|
||||
#[ORM\Column(type: UlidType::NAME, unique: true)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')]
|
||||
public ?Ulid $id = null;
|
||||
private ?Ulid $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'snipContents')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
public ?Snip $snip = null;
|
||||
private ?Snip $snip = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||
public ?self $parent = null;
|
||||
private ?self $parent = null;
|
||||
|
||||
/** @var Collection<int, self> */
|
||||
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
|
||||
public Collection $children;
|
||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
|
||||
private Collection $children;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
public ?string $text = null;
|
||||
private ?string $text = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
public ?array $diff = null;
|
||||
private ?array $diff = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
public ?string $name = null;
|
||||
private ?string $name = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->children = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?Ulid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getSnip(): ?Snip
|
||||
{
|
||||
return $this->snip;
|
||||
}
|
||||
|
||||
public function setSnip(?Snip $snip): self
|
||||
{
|
||||
$this->snip = $snip;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParent(): ?self
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function setParent(?self $parent): self
|
||||
{
|
||||
$this->parent = $parent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public function getChildren(): Collection
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
public function addChild(self $child): self
|
||||
{
|
||||
if (!$this->children->contains($child)) {
|
||||
$this->children->add($child);
|
||||
$child->setParent($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeChild(self $child): self
|
||||
{
|
||||
if ($this->children->removeElement($child)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($child->getParent() === $this) {
|
||||
$child->setParent(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getText(): ?string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
public function setText(?string $text): self
|
||||
{
|
||||
$this->text = $text;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDiff(): ?array
|
||||
{
|
||||
return $this->diff;
|
||||
}
|
||||
|
||||
public function setDiff(?array $diff): static
|
||||
{
|
||||
$this->diff = $diff;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(?string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -16,22 +16,22 @@ class Tag
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
public ?int $id = null;
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotEqualTo(SnipFilterRequest::TAG_ALL)]
|
||||
#[Assert\NotEqualTo(SnipFilterRequest::TAG_NONE)]
|
||||
public ?string $name = null;
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
public ?User $user = null;
|
||||
private ?User $user = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Snip>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Snip::class, inversedBy: 'tags')]
|
||||
public Collection $snips;
|
||||
private Collection $snips;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@ -43,6 +43,43 @@ class Tag
|
||||
return $this->name ?? '';
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Snip>
|
||||
*/
|
||||
public function getSnips(): Collection
|
||||
{
|
||||
return $this->snips;
|
||||
}
|
||||
|
||||
public function addSnip(Snip $snip): static
|
||||
{
|
||||
if (!$this->snips->contains($snip)) {
|
||||
|
@ -34,7 +34,7 @@ class TagsType extends AbstractType implements DataTransformerInterface
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$tags = array_map(fn(Tag $tag) => $tag->name, $value);
|
||||
$tags = array_map(fn(Tag $tag) => $tag->getName(), $value);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@ -51,8 +51,7 @@ class TagsType extends AbstractType implements DataTransformerInterface
|
||||
$tagEntity = $this->repository->findOneBy(['name' => $tag, 'user' => $user]);
|
||||
if ($tagEntity === null) {
|
||||
$tagEntity = new Tag();
|
||||
$tagEntity->name = $tag;
|
||||
$tagEntity->user = $user;
|
||||
$tagEntity->setName($tag)->setUser($user);
|
||||
|
||||
// Validate the new Tag entity
|
||||
$errors = $this->validator->validate($tagEntity);
|
||||
@ -91,4 +90,4 @@ class TagsType extends AbstractType implements DataTransformerInterface
|
||||
{
|
||||
return TextType::class;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,14 +4,13 @@ namespace App\Security\Voter;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class SnipVoter extends Voter
|
||||
{
|
||||
public const string EDIT = 'edit';
|
||||
public const string VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const VIEW = 'view';
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
@ -21,7 +20,7 @@ class SnipVoter extends Voter
|
||||
&& $subject instanceof Snip;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token,/* , ?Vote $vote = null */): bool
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
{
|
||||
/** @var Snip $subject */
|
||||
|
||||
@ -29,14 +28,14 @@ class SnipVoter extends Voter
|
||||
|
||||
switch ($attribute) {
|
||||
case self::VIEW:
|
||||
if ($subject->public) {
|
||||
if ($subject->isPublic()) {
|
||||
return true;
|
||||
}
|
||||
case self::EDIT:
|
||||
if (!$user instanceof UserInterface) {
|
||||
return false;
|
||||
}
|
||||
if ($subject->createdBy === $user) {
|
||||
if ($subject->getCreatedBy() === $user) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
@ -15,26 +15,27 @@ readonly class SnipContentService
|
||||
|
||||
public function update(Snip $snip, string $contents, ?string $contentName): void
|
||||
{
|
||||
$parentContent = $snip->activeVersion;
|
||||
$parentContent = $snip->getActiveVersion();
|
||||
if (self::rebuildText($parentContent) === $contents) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new snipContent entity with previous one as parent
|
||||
$content = new SnipContent();
|
||||
$content->text = $contents;
|
||||
$content->snip = $snip;
|
||||
$content->name = $contentName;
|
||||
|
||||
$content
|
||||
->setText($contents)
|
||||
->setSnip($snip)
|
||||
->setName($contentName)
|
||||
;
|
||||
if ($parentContent !== null) {
|
||||
$content->parent = $parentContent;
|
||||
$content->setParent($parentContent);
|
||||
$this->contentToRelative($parentContent);
|
||||
}
|
||||
|
||||
$this->em->persist($content);
|
||||
$this->em->flush();
|
||||
|
||||
$snip->activeVersion = $content;
|
||||
$snip->setActiveVersion($content);
|
||||
$this->em->persist($snip);
|
||||
$this->em->flush();
|
||||
}
|
||||
@ -44,53 +45,53 @@ readonly class SnipContentService
|
||||
if ($snipContent === null) {
|
||||
return '';
|
||||
}
|
||||
if ($snipContent->text) {
|
||||
return $snipContent->text;
|
||||
if ($snipContent->getText()) {
|
||||
return $snipContent->getText();
|
||||
}
|
||||
|
||||
$parentContent = $snipContent->parent;
|
||||
if ($parentContent === null && $snipContent->diff === null) {
|
||||
$parentContent = $snipContent->getParent();
|
||||
if ($parentContent === null && $snipContent->getDiff() === null) {
|
||||
return '---Something went very wrong, cant rebuild the text---';
|
||||
}
|
||||
|
||||
return MyersDiff::rebuildBFromCompact(
|
||||
self::rebuildText($parentContent), $snipContent->diff
|
||||
self::rebuildText($parentContent), $snipContent->getDiff()
|
||||
);
|
||||
}
|
||||
|
||||
public function setVersion(Snip $snip, SnipContent $version): void
|
||||
{
|
||||
$activeVersion = $snip->activeVersion;
|
||||
$activeVersion = $snip->getActiveVersion();
|
||||
$this->contentToAbsolute($version);
|
||||
$this->contentToRelative($activeVersion);
|
||||
|
||||
$snip->activeVersion = $version;
|
||||
$snip->setActiveVersion($version);
|
||||
$this->em->persist($snip);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function contentToRelative(SnipContent $content): void
|
||||
{
|
||||
if ($content->text === null || $content->parent === null) {
|
||||
if ($content->getText() === null || $content->getParent() === null) {
|
||||
return;
|
||||
}
|
||||
$contentText = $content->text;
|
||||
$parentText = self::rebuildText($content->parent);
|
||||
$contentText = $content->getText();
|
||||
$parentText = self::rebuildText($content->getParent());
|
||||
$diff = MyersDiff::calculate($parentText, $contentText);
|
||||
$content->diff = $diff;
|
||||
$content->text = null;
|
||||
$content->setDiff($diff);
|
||||
$content->setText(null);
|
||||
$this->em->persist($content);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function contentToAbsolute(SnipContent $content): void
|
||||
{
|
||||
if ($content->diff === null) {
|
||||
if ($content->getDiff() === null) {
|
||||
return;
|
||||
}
|
||||
$content->text = self::rebuildText($content);
|
||||
$content->diff = null;
|
||||
$content->setText(self::rebuildText($content));
|
||||
$content->setDiff(null);
|
||||
$this->em->persist($content);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ abstract class AbstractParser implements ParserInterface
|
||||
try {
|
||||
return $this->safeParseView($content);
|
||||
} catch (\Exception $exception) {
|
||||
return sprintf('<pre><code>%s</code></pre>', htmlspecialchars($exception->getMessage()));
|
||||
return sprintf('<pre><code class="hljs">%s</code></pre>', htmlspecialchars($exception->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,9 +17,9 @@ class GenericParser extends AbstractParser
|
||||
$builder = new PipelineBuilder();
|
||||
$pipeline = $builder
|
||||
->add(new HtmlEscapeStage())
|
||||
// ->add(new ReplaceBlocksStage('<pre>', '</pre>', '```'))
|
||||
// ->add(new ReplaceBlocksStage('<code>', '</code>', '``'))
|
||||
->add(new ReplaceStage(PHP_EOL, '<br>'))
|
||||
->add(new ReplaceBlocksStage('<pre><code class="hljs">', '</code></pre>', '```'))
|
||||
->add(new ReplaceBlocksStage('<code class="hljs">', '</code>', '``'))
|
||||
->add($this->referenceStage)
|
||||
->add($this->includeStage)
|
||||
->build()
|
||||
@ -27,4 +27,13 @@ class GenericParser extends AbstractParser
|
||||
|
||||
return $pipeline->process($content);
|
||||
}
|
||||
|
||||
public function parseRaw(string $content): string
|
||||
{
|
||||
return str_replace(
|
||||
['```', '``'],
|
||||
'',
|
||||
$content
|
||||
);
|
||||
}
|
||||
}
|
@ -37,11 +37,11 @@ class IncludeReferenceStage implements StageInterface
|
||||
$content = null;
|
||||
}
|
||||
if ($content) {
|
||||
$snip = $content->snip;
|
||||
$snip = $content->getSnip();
|
||||
} else {
|
||||
$snip = $this->snipRepository->find($id);
|
||||
if ($snip) {
|
||||
$content = $this->snipContentRepository->find($snip->activeVersion);
|
||||
$content = $this->snipContentRepository->find($snip->getActiveVersion());
|
||||
}
|
||||
}
|
||||
if ($content === null) {
|
||||
@ -56,4 +56,4 @@ class IncludeReferenceStage implements StageInterface
|
||||
);
|
||||
}, $payload);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,14 +4,13 @@ namespace App\Service\SnipParser\Generic;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use League\Pipeline\StageInterface;
|
||||
use Tempest\Highlight\Highlighter;
|
||||
|
||||
readonly class ReplaceBlocksStage implements StageInterface
|
||||
class ReplaceBlocksStage implements StageInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $openTag = '<pre><code>',
|
||||
public string $closeTag = '</code></pre>',
|
||||
public string $delimiter = '```'
|
||||
public readonly string $openTag = '<pre><code>',
|
||||
public readonly string $closeTag = '</code></pre>',
|
||||
public readonly string $delimiter = '```'
|
||||
) {}
|
||||
|
||||
public function __invoke(mixed $payload): string
|
||||
@ -27,9 +26,8 @@ readonly class ReplaceBlocksStage implements StageInterface
|
||||
{
|
||||
$pattern = sprintf('/%s(.+?)%s/s', preg_quote($this->delimiter), preg_quote($this->delimiter));
|
||||
|
||||
$highlighter = new Highlighter()->withGutter();
|
||||
return preg_replace_callback($pattern, function ($matches) use ($highlighter) {
|
||||
return $this->openTag . $highlighter->parse(trim($matches[1]), 'php') . $this->closeTag;
|
||||
return preg_replace_callback($pattern, function ($matches) {
|
||||
return $this->openTag . trim($matches[1]) . $this->closeTag;
|
||||
}, $text);
|
||||
}
|
||||
}
|
@ -36,8 +36,8 @@ readonly class UrlReferenceStage implements StageInterface
|
||||
return sprintf('<span title="access denied">%s</span>', $matches[0]);
|
||||
}
|
||||
|
||||
$url = $this->router->generate('snip_single', ['snip' => $snip->id]);
|
||||
$url = $this->router->generate('snip_single', ['snip' => $snip->getId()]);
|
||||
return sprintf('<a href="%s">%s</a>', $url, $snip);
|
||||
}, $payload);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,14 +3,11 @@
|
||||
namespace App\Service\SnipParser\Html;
|
||||
|
||||
use App\Service\SnipParser\AbstractParser;
|
||||
use Tempest\Highlight\Highlighter;
|
||||
|
||||
class HtmlParser extends AbstractParser
|
||||
{
|
||||
public function safeParseView(string $content): string
|
||||
{
|
||||
$highlighter = new Highlighter()->withGutter();
|
||||
|
||||
return '<pre data-lang="html" class="notranslate">' . $highlighter->parse($content, 'html') . '</pre>';
|
||||
return sprintf('<pre><code class="hljs">%s</code></pre>', htmlspecialchars($content));
|
||||
}
|
||||
}
|
@ -6,16 +6,11 @@ use App\Repository\SnipRepository;
|
||||
use App\Service\SnipParser\AbstractParser;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
||||
use League\CommonMark\Extension\DefaultAttributes\DefaultAttributesExtension;
|
||||
use League\CommonMark\Extension\Footnote\FootnoteExtension;
|
||||
use League\CommonMark\Extension\Table\Table;
|
||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Node\Query;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Tempest\Highlight\CommonMark\HighlightExtension;
|
||||
use Tempest\Highlight\Highlighter;
|
||||
|
||||
class MarkdownParser extends AbstractParser
|
||||
{
|
||||
@ -26,25 +21,8 @@ class MarkdownParser extends AbstractParser
|
||||
|
||||
public function safeParseView(string $content): string
|
||||
{
|
||||
$config = [
|
||||
'default_attributes' => [
|
||||
Table::class => [
|
||||
'class' => 'table table-hover',
|
||||
],
|
||||
Link::class => [
|
||||
'class' => 'btn btn-sm btn-secondary',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$converter = new GithubFlavoredMarkdownConverter($config);
|
||||
$converter
|
||||
->getEnvironment()
|
||||
->addExtension(new HighlightExtension(new Highlighter()->withGutter()))
|
||||
->addExtension(new FootnoteExtension())
|
||||
->addExtension(new DefaultAttributesExtension())
|
||||
->addEventListener(DocumentParsedEvent::class, $this->documentParsed(...))
|
||||
;
|
||||
$converter = new GithubFlavoredMarkdownConverter();
|
||||
$converter->getEnvironment()->addEventListener(DocumentParsedEvent::class, $this->documentParsed(...));
|
||||
return $converter->convert($content);
|
||||
}
|
||||
|
||||
@ -54,17 +32,11 @@ class MarkdownParser extends AbstractParser
|
||||
|
||||
$linkNodes = new Query()
|
||||
->where(Query::type(Link::class))
|
||||
->findAll($document)
|
||||
;
|
||||
->findAll($document);
|
||||
|
||||
/** @var Link $linkNode */
|
||||
foreach ($linkNodes as $linkNode) {
|
||||
$url = $linkNode->getUrl();
|
||||
|
||||
if (!is_numeric($url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snip = $this->snipRepo->find($url);
|
||||
if ($snip === null) {
|
||||
continue;
|
||||
|
@ -29,9 +29,9 @@ readonly class ParserFactory
|
||||
|
||||
public function getBySnip(Snip $snip): ParserInterface
|
||||
{
|
||||
$parser = $snip->parser;
|
||||
$parser = $snip->getParser();
|
||||
if (null === $parser) {
|
||||
throw new ServiceNotFoundException(sprintf('Unknown parser for snip "%s"', $snip->parser));
|
||||
throw new ServiceNotFoundException(sprintf('Unknown parser for snip "%s"', $snip->getParser()));
|
||||
}
|
||||
|
||||
return $this->get($parser);
|
||||
@ -49,4 +49,4 @@ readonly class ParserFactory
|
||||
{
|
||||
foreach ($this->getAll() as $parser) yield $parser::getName();
|
||||
}
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ class SnipLoader implements LoaderInterface
|
||||
|
||||
public function getCacheKey(string $name): string
|
||||
{
|
||||
return $this->getFromKey($name)->activeVersion->id;
|
||||
return $this->getFromKey($name)->getActiveVersion()->getId();
|
||||
}
|
||||
|
||||
public function isFresh(string $name, int $time): bool
|
||||
@ -58,4 +58,4 @@ class SnipLoader implements LoaderInterface
|
||||
|
||||
return $snip;
|
||||
}
|
||||
}
|
||||
}
|
@ -56,8 +56,8 @@ class SnipTwigExtension extends AbstractExtension
|
||||
$request = new SnipFilterRequest(SnipFilterRequest::VISIBILITY_ALL, tag: $tag);
|
||||
$snips = $this->snipRepo->findByRequest($user, $request);
|
||||
return array_map(fn(Snip $snip) => [
|
||||
'id' => $snip->id,
|
||||
'name' => $snip->name,
|
||||
'id' => $snip->getId(),
|
||||
'name' => $snip->getName(),
|
||||
], $snips);
|
||||
}
|
||||
}
|
||||
}
|
45
symfony.lock
45
symfony.lock
@ -1,13 +1,4 @@
|
||||
{
|
||||
"doctrine/deprecations": {
|
||||
"version": "1.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
|
||||
}
|
||||
},
|
||||
"doctrine/doctrine-bundle": {
|
||||
"version": "2.14",
|
||||
"recipe": {
|
||||
@ -23,7 +14,7 @@
|
||||
]
|
||||
},
|
||||
"doctrine/doctrine-migrations-bundle": {
|
||||
"version": "3.2",
|
||||
"version": "3.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
@ -48,7 +39,7 @@
|
||||
]
|
||||
},
|
||||
"symfony/debug-bundle": {
|
||||
"version": "6.2",
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
@ -85,15 +76,14 @@
|
||||
]
|
||||
},
|
||||
"symfony/framework-bundle": {
|
||||
"version": "7.3",
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.3",
|
||||
"ref": "5a1497d539f691b96afd45ae397ce5fe30beb4b9"
|
||||
"version": "7.2",
|
||||
"ref": "87bcf6f7c55201f345d8895deda46d2adbdbaa89"
|
||||
},
|
||||
"files": [
|
||||
".editorconfig",
|
||||
"config/packages/cache.yaml",
|
||||
"config/packages/framework.yaml",
|
||||
"config/preload.php",
|
||||
@ -105,7 +95,7 @@
|
||||
]
|
||||
},
|
||||
"symfony/maker-bundle": {
|
||||
"version": "1.48",
|
||||
"version": "1.63",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
@ -125,18 +115,6 @@
|
||||
"config/packages/monolog.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/property-info": {
|
||||
"version": "7.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.3",
|
||||
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/property_info.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
@ -183,8 +161,7 @@
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||
},
|
||||
"files": []
|
||||
}
|
||||
},
|
||||
"symfony/validator": {
|
||||
"version": "7.2",
|
||||
@ -199,12 +176,12 @@
|
||||
]
|
||||
},
|
||||
"symfony/web-profiler-bundle": {
|
||||
"version": "7.3",
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.3",
|
||||
"ref": "a363460c1b0b4a4d0242f2ce1a843ca0f6ac9026"
|
||||
"version": "6.1",
|
||||
"ref": "8b51135b84f4266e3b4c8a6dc23c9d1e32e543b7"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/web_profiler.yaml",
|
||||
@ -212,6 +189,6 @@
|
||||
]
|
||||
},
|
||||
"twig/extra-bundle": {
|
||||
"version": "v3.5.1"
|
||||
"version": "v3.21.0"
|
||||
}
|
||||
}
|
||||
|
@ -53,4 +53,22 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||
<script>
|
||||
const codeBlocks = document.querySelectorAll('code.hljs');
|
||||
|
||||
codeBlocks.forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
|
||||
{% set title = 'Create Snip' %}
|
||||
|
||||
{% block body %}
|
||||
{{ form(form) }}
|
||||
{% endblock %}
|
@ -1,6 +1,10 @@
|
||||
{% extends 'snip/base.html.twig' %}
|
||||
|
||||
{% set title %}{{ snip }} - Edit{% endset %}
|
||||
{% if snip.id %}
|
||||
{% set title %}{{ snip }} - Edit{% endset %}
|
||||
{% else %}
|
||||
{% set title = 'Create Snip' %}
|
||||
{% endif %}
|
||||
{% set active = 'edit' %}
|
||||
|
||||
{% block buttons %}
|
||||
|
@ -15,5 +15,18 @@
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="{{ asset('github-light-default.css') }}">
|
||||
<link rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||
<script>
|
||||
const codeBlocks = document.querySelectorAll('code.hljs');
|
||||
|
||||
codeBlocks.forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user