siinus
3 years ago
commit
1abf940681
49 changed files with 6433 additions and 0 deletions
@ -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. |
||||||
|
# |
||||||
|
# Run "composer dump-env prod" 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=prod |
||||||
|
APP_SECRET=28fae772fdc3e2610b7eb0c14f7c5291 |
||||||
|
###< symfony/framework-bundle ### |
||||||
|
|
||||||
|
DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=mariadb-10.3.32" |
||||||
|
|
||||||
|
HASH_SALT="theQuickBrownFoxJumpsOverTheLazyDog" |
@ -0,0 +1,10 @@ |
|||||||
|
|
||||||
|
###> symfony/framework-bundle ### |
||||||
|
/.env.local |
||||||
|
/.env.local.php |
||||||
|
/.env.*.local |
||||||
|
/config/secrets/prod/prod.decrypt.private.php |
||||||
|
/public/bundles/ |
||||||
|
/var/ |
||||||
|
/vendor/ |
||||||
|
###< symfony/framework-bundle ### |
@ -0,0 +1,13 @@ |
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE |
||||||
|
Version 2, December 2004 |
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> |
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified |
||||||
|
copies of this license document, and changing it is allowed as long |
||||||
|
as the name is changed. |
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE |
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION |
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO. |
@ -0,0 +1,31 @@ |
|||||||
|
# Lyhendi |
||||||
|
|
||||||
|
Demo/PoC app doing url shortening. Allows user to insert long url and (optionally) custom string. |
||||||
|
|
||||||
|
Generated url strings are based on auto increment id keys and collisions are *unlikely*. |
||||||
|
|
||||||
|
### Obviously not for production use |
||||||
|
|
||||||
|
- No authentication |
||||||
|
- No administration |
||||||
|
- No captcha |
||||||
|
- Not tested by competent QA engineer |
||||||
|
- Index page displays the last url shortened, for demo purposes |
||||||
|
- Does not redirect to target url. For demo, we don't need it |
||||||
|
|
||||||
|
### Futureproof |
||||||
|
|
||||||
|
- Relatively fresh platform (PHP8.1, MariaDB 10.3, Symfony 6.0) |
||||||
|
- Lightweight and ready for further development |
||||||
|
- Easy to add features (auth, admin, design etc...) |
||||||
|
- Internet is full of unreasonably lengthy urls, which should be shortened |
||||||
|
|
||||||
|
## Setup |
||||||
|
|
||||||
|
After cloning, cd to project dir and run ```docker-compose up -d``` |
||||||
|
|
||||||
|
Assume that the port 8080 is not in use, application should be available at http://localhost:8080 |
||||||
|
|
||||||
|
One can switch `APP_ENV` value to `dev` and enjoy the almighty Symfony debug bar and profiler. |
||||||
|
|
||||||
|
*Developed and tested only on linux machine, YMMV* |
@ -0,0 +1,17 @@ |
|||||||
|
#!/usr/bin/env php |
||||||
|
<?php |
||||||
|
|
||||||
|
use App\Kernel; |
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application; |
||||||
|
|
||||||
|
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); |
||||||
|
}; |
@ -0,0 +1,79 @@ |
|||||||
|
{ |
||||||
|
"type": "project", |
||||||
|
"license": "proprietary", |
||||||
|
"minimum-stability": "stable", |
||||||
|
"prefer-stable": true, |
||||||
|
"require": { |
||||||
|
"php": ">=8.1.2", |
||||||
|
"ext-ctype": "*", |
||||||
|
"ext-iconv": "*", |
||||||
|
"doctrine/doctrine-bundle": "^2.5", |
||||||
|
"doctrine/doctrine-migrations-bundle": "^3.2", |
||||||
|
"doctrine/orm": "^2.11", |
||||||
|
"hashids/hashids": "^4.1", |
||||||
|
"symfony/console": "6.0.*", |
||||||
|
"symfony/dotenv": "6.0.*", |
||||||
|
"symfony/flex": "^2", |
||||||
|
"symfony/form": "6.0.*", |
||||||
|
"symfony/framework-bundle": "6.0.*", |
||||||
|
"symfony/proxy-manager-bridge": "6.0.*", |
||||||
|
"symfony/runtime": "6.0.*", |
||||||
|
"symfony/validator": "6.0.*", |
||||||
|
"symfony/yaml": "6.0.*" |
||||||
|
}, |
||||||
|
"config": { |
||||||
|
"allow-plugins": { |
||||||
|
"composer/package-versions-deprecated": true, |
||||||
|
"symfony/flex": true, |
||||||
|
"symfony/runtime": true |
||||||
|
}, |
||||||
|
"optimize-autoloader": true, |
||||||
|
"preferred-install": { |
||||||
|
"*": "dist" |
||||||
|
}, |
||||||
|
"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": "*" |
||||||
|
}, |
||||||
|
"scripts": { |
||||||
|
"auto-scripts": { |
||||||
|
"cache:clear": "symfony-cmd", |
||||||
|
"assets:install %PUBLIC_DIR%": "symfony-cmd" |
||||||
|
}, |
||||||
|
"post-install-cmd": [ |
||||||
|
"@auto-scripts" |
||||||
|
], |
||||||
|
"post-update-cmd": [ |
||||||
|
"@auto-scripts" |
||||||
|
] |
||||||
|
}, |
||||||
|
"conflict": { |
||||||
|
"symfony/symfony": "*" |
||||||
|
}, |
||||||
|
"extra": { |
||||||
|
"symfony": { |
||||||
|
"allow-contrib": false, |
||||||
|
"require": "6.0.*" |
||||||
|
} |
||||||
|
}, |
||||||
|
"require-dev": { |
||||||
|
"symfony/stopwatch": "6.0.*", |
||||||
|
"symfony/web-profiler-bundle": "6.0.*" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
<?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\TwigBundle\TwigBundle::class => ['all' => true], |
||||||
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], |
||||||
|
]; |
@ -0,0 +1,19 @@ |
|||||||
|
framework: |
||||||
|
cache: |
||||||
|
# Unique name of your app: used to compute stable namespaces for cache keys. |
||||||
|
#prefix_seed: your_vendor_name/app_name |
||||||
|
|
||||||
|
# The "app" cache stores to the filesystem by default. |
||||||
|
# The data in this cache should persist between deploys. |
||||||
|
# Other options include: |
||||||
|
|
||||||
|
# Redis |
||||||
|
#app: cache.adapter.redis |
||||||
|
#default_redis_provider: redis://localhost |
||||||
|
|
||||||
|
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) |
||||||
|
#app: cache.adapter.apcu |
||||||
|
|
||||||
|
# Namespaced pools use the above "app" backend by default |
||||||
|
#pools: |
||||||
|
#my.dedicated.cache: null |
@ -0,0 +1,6 @@ |
|||||||
|
web_profiler: |
||||||
|
toolbar: true |
||||||
|
intercept_redirects: false |
||||||
|
|
||||||
|
framework: |
||||||
|
profiler: { only_exceptions: false } |
@ -0,0 +1,17 @@ |
|||||||
|
doctrine: |
||||||
|
dbal: |
||||||
|
url: '%env(resolve:DATABASE_URL)%' |
||||||
|
|
||||||
|
# IMPORTANT: You MUST configure your server version, |
||||||
|
# either here or in the DATABASE_URL env var (see .env file) |
||||||
|
#server_version: '13' |
||||||
|
orm: |
||||||
|
auto_generate_proxy_classes: true |
||||||
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware |
||||||
|
auto_mapping: true |
||||||
|
mappings: |
||||||
|
App: |
||||||
|
is_bundle: false |
||||||
|
dir: '%kernel.project_dir%/src/Entity' |
||||||
|
prefix: 'App\Entity' |
||||||
|
alias: App |
@ -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: '%kernel.debug%' |
@ -0,0 +1,24 @@ |
|||||||
|
# see https://symfony.com/doc/current/reference/configuration/framework.html |
||||||
|
framework: |
||||||
|
secret: '%env(APP_SECRET)%' |
||||||
|
#csrf_protection: true |
||||||
|
http_method_override: false |
||||||
|
|
||||||
|
# Enables session support. Note that the session will ONLY be started if you read or write from it. |
||||||
|
# Remove or comment this section to explicitly disable session support. |
||||||
|
session: |
||||||
|
handler_id: null |
||||||
|
cookie_secure: auto |
||||||
|
cookie_samesite: lax |
||||||
|
storage_factory_id: session.storage.factory.native |
||||||
|
|
||||||
|
#esi: true |
||||||
|
#fragments: true |
||||||
|
php_errors: |
||||||
|
log: true |
||||||
|
|
||||||
|
when@test: |
||||||
|
framework: |
||||||
|
test: true |
||||||
|
session: |
||||||
|
storage_factory_id: session.storage.factory.mock_file |
@ -0,0 +1,17 @@ |
|||||||
|
doctrine: |
||||||
|
orm: |
||||||
|
auto_generate_proxy_classes: false |
||||||
|
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,12 @@ |
|||||||
|
framework: |
||||||
|
router: |
||||||
|
utf8: true |
||||||
|
|
||||||
|
# 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,4 @@ |
|||||||
|
doctrine: |
||||||
|
dbal: |
||||||
|
# "TEST_TOKEN" is typically set by ParaTest |
||||||
|
dbname_suffix: '_test%env(default::TEST_TOKEN)%' |
@ -0,0 +1,3 @@ |
|||||||
|
framework: |
||||||
|
validation: |
||||||
|
not_compromised_password: false |
@ -0,0 +1,6 @@ |
|||||||
|
web_profiler: |
||||||
|
toolbar: false |
||||||
|
intercept_redirects: false |
||||||
|
|
||||||
|
framework: |
||||||
|
profiler: { collect: false } |
@ -0,0 +1,6 @@ |
|||||||
|
twig: |
||||||
|
default_path: '%kernel.project_dir%/templates' |
||||||
|
|
||||||
|
when@test: |
||||||
|
twig: |
||||||
|
strict_variables: true |
@ -0,0 +1,8 @@ |
|||||||
|
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\: [] |
@ -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'; |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
controllers: |
||||||
|
resource: ../src/Controller/ |
||||||
|
type: annotation |
||||||
|
|
||||||
|
kernel: |
||||||
|
resource: ../src/Kernel.php |
||||||
|
type: annotation |
@ -0,0 +1,7 @@ |
|||||||
|
controllers: |
||||||
|
resource: ../../src/Controller/ |
||||||
|
type: annotation |
||||||
|
|
||||||
|
kernel: |
||||||
|
resource: ../../src/Kernel.php |
||||||
|
type: annotation |
@ -0,0 +1,7 @@ |
|||||||
|
web_profiler_wdt: |
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' |
||||||
|
prefix: /_wdt |
||||||
|
|
||||||
|
web_profiler_profiler: |
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' |
||||||
|
prefix: /_profiler |
@ -0,0 +1,4 @@ |
|||||||
|
when@dev: |
||||||
|
_errors: |
||||||
|
resource: '@FrameworkBundle/Resources/config/routing/errors.xml' |
||||||
|
prefix: /_error |
@ -0,0 +1,30 @@ |
|||||||
|
# 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: |
||||||
|
container.dumper.inline_factories: true |
||||||
|
hasher.salt: '%env(resolve:HASH_SALT)%' |
||||||
|
|
||||||
|
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 |
||||||
|
|
||||||
|
App\Hasher: |
||||||
|
arguments: |
||||||
|
$salt: '%hasher.salt%' |
@ -0,0 +1,42 @@ |
|||||||
|
version: '3' |
||||||
|
services: |
||||||
|
# Nginx server configuration |
||||||
|
web: |
||||||
|
image: nginx:1.21.6 |
||||||
|
depends_on: |
||||||
|
- db |
||||||
|
- php |
||||||
|
links: |
||||||
|
- db |
||||||
|
- php |
||||||
|
volumes: |
||||||
|
- .:/var/www/html |
||||||
|
- ./docker/nginx:/etc/nginx/conf.d/ |
||||||
|
ports: |
||||||
|
- "8080:80" |
||||||
|
# Php-fpm configuration |
||||||
|
php: |
||||||
|
build: |
||||||
|
context: . |
||||||
|
dockerfile: docker/php/Dockerfile |
||||||
|
volumes: |
||||||
|
- .:/var/www/html |
||||||
|
- ./docker/php:/usr/local/etc/php/php.ini |
||||||
|
depends_on: |
||||||
|
- db |
||||||
|
# MariaDB configuration |
||||||
|
db: |
||||||
|
image: mariadb:10.3.32 |
||||||
|
restart: always |
||||||
|
environment: |
||||||
|
- MYSQL_DATABASE=db |
||||||
|
- MYSQL_ROOT_PASSWORD=db |
||||||
|
- MYSQL_USER=db |
||||||
|
- MYSQL_PASSWORD=db |
||||||
|
ports: |
||||||
|
- "3306:3306" |
||||||
|
volumes: |
||||||
|
- /dbdata:/var/lib/mysql |
||||||
|
|
||||||
|
volumes: |
||||||
|
dbdata: |
@ -0,0 +1,26 @@ |
|||||||
|
server { |
||||||
|
server_name _; |
||||||
|
root /var/www/html/public; |
||||||
|
fastcgi_buffers 16 16k; |
||||||
|
fastcgi_buffer_size 32k; |
||||||
|
|
||||||
|
location / { |
||||||
|
try_files $uri /index.php$is_args$args; |
||||||
|
} |
||||||
|
# PHP-FPM Configuration Nginx |
||||||
|
location ~ ^/index\.php(/|$) { |
||||||
|
try_files $uri = 404; |
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.+)$; |
||||||
|
fastcgi_pass php:9000; |
||||||
|
include fastcgi_params; |
||||||
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; |
||||||
|
fastcgi_param DOCUMENT_ROOT $realpath_root; |
||||||
|
} |
||||||
|
|
||||||
|
location ~ \.php$ { |
||||||
|
return 404; |
||||||
|
} |
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log; |
||||||
|
error_log /var/log/nginx/error.log; |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
FROM php:8.1.2-fpm |
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y unzip \ |
||||||
|
&& docker-php-ext-install opcache pdo pdo_mysql bcmath |
||||||
|
|
||||||
|
# Install Composer |
||||||
|
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer |
||||||
|
|
||||||
|
CMD composer install ; bin/console doctrine:migrations:migrate ; php-fpm |
@ -0,0 +1,31 @@ |
|||||||
|
<?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 Version20220129073043 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 url (id INT AUTO_INCREMENT NOT NULL, long_url LONGTEXT NOT NULL, custom_string VARCHAR(11) DEFAULT NULL, UNIQUE INDEX UNIQ_F47645AEAA1366AD (custom_string), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); |
||||||
|
} |
||||||
|
|
||||||
|
public function down(Schema $schema): void |
||||||
|
{ |
||||||
|
// this down() migration is auto-generated, please modify it to your needs |
||||||
|
$this->addSql('DROP TABLE url'); |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 15 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,91 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Controller; |
||||||
|
|
||||||
|
use App\Entity\Url; |
||||||
|
use App\Form\Type\LyhendiType; |
||||||
|
use App\Hasher; |
||||||
|
use App\Repository\UrlRepository; |
||||||
|
use Doctrine\ORM\EntityManagerInterface; |
||||||
|
use Doctrine\Persistence\ManagerRegistry; |
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||||
|
use Symfony\Component\Form\FormInterface; |
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse; |
||||||
|
use Symfony\Component\HttpFoundation\Request; |
||||||
|
use Symfony\Component\HttpFoundation\Response; |
||||||
|
use Symfony\Component\Routing\Annotation\Route; |
||||||
|
|
||||||
|
final class LyhendiController extends AbstractController |
||||||
|
{ |
||||||
|
public function __construct(private ManagerRegistry $managerRegistry, private Hasher $hasher) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @Route("/", methods={"GET"}) |
||||||
|
*/ |
||||||
|
public function index(): Response |
||||||
|
{ |
||||||
|
$form = $this->createLyhendiForm(); |
||||||
|
$lastResult = $this->getRepository()->findOneBy([], ['id' => 'DESC']); |
||||||
|
return $this->renderForm('index.html.twig', ['form' => $form, 'last' => $lastResult]); |
||||||
|
} |
||||||
|
|
||||||
|
private function createLyhendiForm(): FormInterface |
||||||
|
{ |
||||||
|
return $this->createForm(LyhendiType::class); |
||||||
|
} |
||||||
|
|
||||||
|
private function getRepository(): UrlRepository |
||||||
|
{ |
||||||
|
/** @noinspection PhpIncompatibleReturnTypeInspection */ |
||||||
|
return $this->getEntityManager()->getRepository(Url::class); |
||||||
|
} |
||||||
|
|
||||||
|
private function getEntityManager(): EntityManagerInterface |
||||||
|
{ |
||||||
|
/** @noinspection PhpIncompatibleReturnTypeInspection */ |
||||||
|
return $this->managerRegistry->getManagerForClass(Url::class); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @Route("/", methods={"POST"}) |
||||||
|
*/ |
||||||
|
public function insert(Request $request): Response |
||||||
|
{ |
||||||
|
$form = $this->createForm(LyhendiType::class); |
||||||
|
$form->handleRequest($request); |
||||||
|
if (!$form->isSubmitted()) { |
||||||
|
return $this->getRedirectToHome(); |
||||||
|
} |
||||||
|
if ($form->isValid()) { |
||||||
|
/** @var Url $url */ |
||||||
|
$url = $form->getData(); |
||||||
|
$manager = $this->getEntityManager(); |
||||||
|
$manager->persist($url); |
||||||
|
$manager->flush(); |
||||||
|
$publicString = $url->getCustomString() ?? $this->hasher->encode($url->getId()); |
||||||
|
return $this->redirectToRoute('app_lyhendi_get', ['string' => $publicString]); |
||||||
|
} |
||||||
|
return $this->renderForm('index.html.twig', ['form' => $form, 'last' => null]); |
||||||
|
} |
||||||
|
|
||||||
|
private function getRedirectToHome(): RedirectResponse |
||||||
|
{ |
||||||
|
return $this->redirectToRoute('app_lyhendi_index'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @Route("/{string}", methods={"GET"}) |
||||||
|
*/ |
||||||
|
public function get(string $string): Response |
||||||
|
{ |
||||||
|
$url = $this->getRepository()->findOneByAny($string); |
||||||
|
if ($url === null) { |
||||||
|
return $this->getRedirectToHome(); |
||||||
|
} |
||||||
|
return new Response($url->getLongUrl()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Entity; |
||||||
|
|
||||||
|
use App\Validator as AppAssert; |
||||||
|
use Doctrine\ORM\Mapping as ORM; |
||||||
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; |
||||||
|
use Symfony\Component\Validator\Constraints as Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* @ORM\Entity(repositoryClass="App\Repository\UrlRepository") |
||||||
|
* @UniqueEntity("customString") |
||||||
|
*/ |
||||||
|
class Url |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @ORM\Id |
||||||
|
* @ORM\GeneratedValue |
||||||
|
* @ORM\Column(type="integer") |
||||||
|
*/ |
||||||
|
private ?int $id; |
||||||
|
|
||||||
|
/** |
||||||
|
* @ORM\Column(type="text") |
||||||
|
* @Assert\NotBlank |
||||||
|
* @Assert\Url |
||||||
|
*/ |
||||||
|
private ?string $longUrl; |
||||||
|
|
||||||
|
/** |
||||||
|
* @ORM\Column(type="string", length=11, nullable=true, unique=true) |
||||||
|
* @Assert\Length(min=5, max=11) |
||||||
|
* @AppAssert\CustomStringNeverCollides |
||||||
|
*/ |
||||||
|
private ?string $customString = null; |
||||||
|
|
||||||
|
public function getId(): int |
||||||
|
{ |
||||||
|
return $this->id; |
||||||
|
} |
||||||
|
|
||||||
|
public function getLongUrl(): string |
||||||
|
{ |
||||||
|
return $this->longUrl; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string|null $longUrl |
||||||
|
* @return Url |
||||||
|
*/ |
||||||
|
public function setLongUrl(?string $longUrl): Url |
||||||
|
{ |
||||||
|
$this->longUrl = $longUrl; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function getCustomString(): ?string |
||||||
|
{ |
||||||
|
return $this->customString; |
||||||
|
} |
||||||
|
|
||||||
|
public function setCustomString(?string $customString): Url |
||||||
|
{ |
||||||
|
$this->customString = $customString; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Exception; |
||||||
|
|
||||||
|
use RuntimeException; |
||||||
|
|
||||||
|
class InvalidHashException extends RuntimeException |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Form\Type; |
||||||
|
|
||||||
|
use App\Entity\Url; |
||||||
|
use Symfony\Component\Form\AbstractType; |
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType; |
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType; |
||||||
|
use Symfony\Component\Form\FormBuilderInterface; |
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver; |
||||||
|
|
||||||
|
class LyhendiType extends AbstractType |
||||||
|
{ |
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void |
||||||
|
{ |
||||||
|
$builder |
||||||
|
->add('longUrl', TextType::class) |
||||||
|
->add('customString', TextType::class, [ |
||||||
|
'required' => false, |
||||||
|
]) |
||||||
|
->add('submit', SubmitType::class); |
||||||
|
} |
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void |
||||||
|
{ |
||||||
|
$resolver->setDefaults([ |
||||||
|
'data_class' => Url::class, |
||||||
|
'csrf_protection' => true, |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App; |
||||||
|
|
||||||
|
use App\Exception\InvalidHashException; |
||||||
|
use Hashids\Hashids; |
||||||
|
use Hashids\HashidsInterface; |
||||||
|
|
||||||
|
class Hasher |
||||||
|
{ |
||||||
|
private HashidsInterface $hashids; |
||||||
|
|
||||||
|
public function __construct(?string $salt = null) |
||||||
|
{ |
||||||
|
$salt = $salt ?? 'default_salt'; |
||||||
|
$this->hashids = new Hashids($salt, 11); |
||||||
|
} |
||||||
|
|
||||||
|
public function encode(int $id): string |
||||||
|
{ |
||||||
|
return $this->hashids->encode($id); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @throws InvalidHashException |
||||||
|
*/ |
||||||
|
public function decode(string $hash): int |
||||||
|
{ |
||||||
|
$decoded = $this->hashids->decode($hash); |
||||||
|
if (count($decoded) === 0) { |
||||||
|
throw new InvalidHashException('Hash not valid'); |
||||||
|
} |
||||||
|
return $decoded[0]; |
||||||
|
} |
||||||
|
|
||||||
|
public function isValid(string $hash): bool |
||||||
|
{ |
||||||
|
return count($this->hashids->decode($hash)) !== 0; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace App; |
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; |
||||||
|
use Symfony\Component\HttpKernel\Kernel as BaseKernel; |
||||||
|
|
||||||
|
class Kernel extends BaseKernel |
||||||
|
{ |
||||||
|
use MicroKernelTrait; |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Repository; |
||||||
|
|
||||||
|
use App\Entity\Url; |
||||||
|
use App\Exception\InvalidHashException; |
||||||
|
use App\Hasher; |
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; |
||||||
|
use Doctrine\Persistence\ManagerRegistry; |
||||||
|
|
||||||
|
final class UrlRepository extends ServiceEntityRepository |
||||||
|
{ |
||||||
|
public function __construct(ManagerRegistry $registry, private Hasher $hasher) |
||||||
|
{ |
||||||
|
parent::__construct($registry, Url::class); |
||||||
|
} |
||||||
|
|
||||||
|
public function findOneByAny(string $string): ?Url |
||||||
|
{ |
||||||
|
$found = $this->findOneByCustomString($string); |
||||||
|
if ($found === null) { |
||||||
|
$found = $this->findOneByHash($string); |
||||||
|
} |
||||||
|
return $found; |
||||||
|
} |
||||||
|
|
||||||
|
public function findOneByCustomString(string $customString): ?Url |
||||||
|
{ |
||||||
|
return $this->findOneBy(['customString' => $customString]); |
||||||
|
} |
||||||
|
|
||||||
|
public function findOneByHash(string $hash): ?Url |
||||||
|
{ |
||||||
|
try { |
||||||
|
$id = $this->hasher->decode($hash); |
||||||
|
return $this->find($id); |
||||||
|
} catch (InvalidHashException $e) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Twig; |
||||||
|
|
||||||
|
use App\Entity\Url; |
||||||
|
use App\Hasher; |
||||||
|
use Twig\Extension\AbstractExtension; |
||||||
|
use Twig\TwigFunction; |
||||||
|
|
||||||
|
class HasherExtension extends AbstractExtension |
||||||
|
{ |
||||||
|
public function __construct(private Hasher $hasher) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function getFunctions() |
||||||
|
{ |
||||||
|
return [ |
||||||
|
new TwigFunction('hasher_encode', [$this, 'encode']), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public function encode(Url $url): string |
||||||
|
{ |
||||||
|
return $url->getCustomString() ?? $this->hasher->encode($url->getId()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Validator; |
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint; |
||||||
|
|
||||||
|
/** |
||||||
|
* @Annotation |
||||||
|
* @Target({"PROPERTY", "METHOD", "ANNOTATION"}) |
||||||
|
*/ |
||||||
|
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] |
||||||
|
class CustomStringNeverCollides extends Constraint |
||||||
|
{ |
||||||
|
public string $message = 'This value already exists, or is reserved.'; |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Validator; |
||||||
|
|
||||||
|
use App\Hasher; |
||||||
|
use Symfony\Component\Validator\Constraint; |
||||||
|
use Symfony\Component\Validator\ConstraintValidator; |
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException; |
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedValueException; |
||||||
|
|
||||||
|
final class CustomStringNeverCollidesValidator extends ConstraintValidator |
||||||
|
{ |
||||||
|
public function __construct(private Hasher $hasher) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @inheritDoc |
||||||
|
*/ |
||||||
|
public function validate(mixed $value, Constraint $constraint): void |
||||||
|
{ |
||||||
|
if (!$constraint instanceof CustomStringNeverCollides) { |
||||||
|
throw new UnexpectedTypeException($constraint, CustomStringNeverCollides::class); |
||||||
|
} |
||||||
|
|
||||||
|
if (null === $value || '' === $value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!is_string($value)) { |
||||||
|
throw new UnexpectedValueException($value, 'string'); |
||||||
|
} |
||||||
|
|
||||||
|
/** Can be decoded to id, so avoid it */ |
||||||
|
if ($this->hasher->isValid($value)) { |
||||||
|
$this->context->buildViolation($constraint->message)->addViolation(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,301 @@ |
|||||||
|
{ |
||||||
|
"doctrine/annotations": { |
||||||
|
"version": "1.13", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "1.0", |
||||||
|
"ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"config/routes/annotations.yaml" |
||||||
|
] |
||||||
|
}, |
||||||
|
"doctrine/cache": { |
||||||
|
"version": "2.1.1" |
||||||
|
}, |
||||||
|
"doctrine/collections": { |
||||||
|
"version": "1.6.8" |
||||||
|
}, |
||||||
|
"doctrine/common": { |
||||||
|
"version": "3.2.1" |
||||||
|
}, |
||||||
|
"doctrine/dbal": { |
||||||
|
"version": "3.3.0" |
||||||
|
}, |
||||||
|
"doctrine/deprecations": { |
||||||
|
"version": "v0.5.3" |
||||||
|
}, |
||||||
|
"doctrine/doctrine-bundle": { |
||||||
|
"version": "2.5", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "2.4", |
||||||
|
"ref": "f98f1affe028f8153a459d15f220ada3826b5aa2" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"config/packages/doctrine.yaml", |
||||||
|
"config/packages/prod/doctrine.yaml", |
||||||
|
"config/packages/test/doctrine.yaml", |
||||||
|
"src/Entity/.gitignore", |
||||||
|
"src/Repository/.gitignore" |
||||||
|
] |
||||||
|
}, |
||||||
|
"doctrine/doctrine-migrations-bundle": { |
||||||
|
"version": "3.2", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "3.1", |
||||||
|
"ref": "ee609429c9ee23e22d6fa5728211768f51ed2818" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"config/packages/doctrine_migrations.yaml", |
||||||
|
"migrations/.gitignore" |
||||||
|
] |
||||||
|
}, |
||||||
|
"doctrine/event-manager": { |
||||||
|
"version": "1.1.1" |
||||||
|
}, |
||||||
|
"doctrine/inflector": { |
||||||
|
"version": "2.0.4" |
||||||
|
}, |
||||||
|
"doctrine/instantiator": { |
||||||
|
"version": "1.4.0" |
||||||
|
}, |
||||||
|
"doctrine/lexer": { |
||||||
|
"version": "1.2.2" |
||||||
|
}, |
||||||
|
"doctrine/migrations": { |
||||||
|
"version": "3.4.0" |
||||||
|
}, |
||||||
|
"doctrine/orm": { |
||||||
|
"version": "2.11.0" |
||||||
|
}, |
||||||
|
"doctrine/persistence": { |
||||||
|
"version": "2.3.0" |
||||||
|
}, |
||||||
|
"doctrine/sql-formatter": { |
||||||
|
"version": "1.1.2" |
||||||
|
}, |
||||||
|
"friendsofphp/proxy-manager-lts": { |
||||||
|
"version": "v1.0.5" |
||||||
|
}, |
||||||
|
"hashids/hashids": { |
||||||
|
"version": "4.1.0" |
||||||
|
}, |
||||||
|
"laminas/laminas-code": { |
||||||
|
"version": "4.5.1" |
||||||
|
}, |
||||||
|
"psr/cache": { |
||||||
|
"version": "3.0.0" |
||||||
|
}, |
||||||
|
"psr/container": { |
||||||
|
"version": "2.0.2" |
||||||
|
}, |
||||||
|
"psr/event-dispatcher": { |
||||||
|
"version": "1.0.0" |
||||||
|
}, |
||||||
|
"psr/log": { |
||||||
|
"version": "3.0.0" |
||||||
|
}, |
||||||
|
"symfony/cache": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/cache-contracts": { |
||||||
|
"version": "v3.0.0" |
||||||
|
}, |
||||||
|
"symfony/config": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/console": { |
||||||
|
"version": "6.0", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "5.3", |
||||||
|
"ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"bin/console" |
||||||
|
] |
||||||
|
}, |
||||||
|
"symfony/dependency-injection": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/deprecation-contracts": { |
||||||
|
"version": "v3.0.0" |
||||||
|
}, |
||||||
|
"symfony/doctrine-bridge": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/dotenv": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/error-handler": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/event-dispatcher": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/event-dispatcher-contracts": { |
||||||
|
"version": "v3.0.0" |
||||||
|
}, |
||||||
|
"symfony/filesystem": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/finder": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/flex": { |
||||||
|
"version": "2.1", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "1.0", |
||||||
|
"ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
".env" |
||||||
|
] |
||||||
|
}, |
||||||
|
"symfony/form": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/framework-bundle": { |
||||||
|
"version": "6.0", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "5.4", |
||||||
|
"ref": "3cd216a4d007b78d8554d44a5b1c0a446dab24fb" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"config/packages/cache.yaml", |
||||||
|
"config/packages/framework.yaml", |
||||||
|
"config/preload.php", |
||||||
|
"config/routes/framework.yaml", |
||||||
|
"config/services.yaml", |
||||||
|
"public/index.php", |
||||||
|
"src/Controller/.gitignore", |
||||||
|
"src/Kernel.php" |
||||||
|
] |
||||||
|
}, |
||||||
|
"symfony/http-foundation": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/http-kernel": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/options-resolver": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/polyfill-intl-grapheme": { |
||||||
|
"version": "v1.24.0" |
||||||
|
}, |
||||||
|
"symfony/polyfill-intl-icu": { |
||||||
|
"version": "v1.24.0" |
||||||
|
}, |
||||||
|
"symfony/polyfill-intl-normalizer": { |
||||||
|
"version": "v1.24.0" |
||||||
|
}, |
||||||
|
"symfony/polyfill-mbstring": { |
||||||
|
"version": "v1.24.0" |
||||||
|
}, |
||||||
|
"symfony/polyfill-php81": { |
||||||
|
"version": "v1.24.0" |
||||||
|
}, |
||||||
|
"symfony/property-access": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/property-info": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/proxy-manager-bridge": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/routing": { |
||||||
|
"version": "6.0", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "6.0", |
||||||
|
"ref": "ab9ad892b7bba7ac584f6dc2ccdb659d358c63c5" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"config/packages/routing.yaml", |
||||||
|
"config/routes.yaml" |
||||||
|
] |
||||||
|
}, |
||||||
|
"symfony/runtime": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/service-contracts": { |
||||||
|
"version": "v3.0.0" |
||||||
|
}, |
||||||
|
"symfony/stopwatch": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/string": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/translation-contracts": { |
||||||
|
"version": "v3.0.0" |
||||||
|
}, |
||||||
|
"symfony/twig-bridge": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/twig-bundle": { |
||||||
|
"version": "6.0", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "5.4", |
||||||
|
"ref": "bffbb8f1a849736e64006735afae730cb428b6ff" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"config/packages/twig.yaml", |
||||||
|
"templates/base.html.twig" |
||||||
|
] |
||||||
|
}, |
||||||
|
"symfony/validator": { |
||||||
|
"version": "6.0", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "4.3", |
||||||
|
"ref": "3eb8df139ec05414489d55b97603c5f6ca0c44cb" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"config/packages/test/validator.yaml", |
||||||
|
"config/packages/validator.yaml" |
||||||
|
] |
||||||
|
}, |
||||||
|
"symfony/var-dumper": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/var-exporter": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"symfony/web-profiler-bundle": { |
||||||
|
"version": "6.0", |
||||||
|
"recipe": { |
||||||
|
"repo": "github.com/symfony/recipes", |
||||||
|
"branch": "master", |
||||||
|
"version": "3.3", |
||||||
|
"ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"config/packages/dev/web_profiler.yaml", |
||||||
|
"config/packages/test/web_profiler.yaml", |
||||||
|
"config/routes/dev/web_profiler.yaml" |
||||||
|
] |
||||||
|
}, |
||||||
|
"symfony/yaml": { |
||||||
|
"version": "v6.0.3" |
||||||
|
}, |
||||||
|
"twig/twig": { |
||||||
|
"version": "v3.3.7" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<title>{% block title %}Welcome!{% endblock %}</title> |
||||||
|
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #} |
||||||
|
{% block stylesheets %} |
||||||
|
{{ encore_entry_link_tags('app') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block javascripts %} |
||||||
|
{{ encore_entry_script_tags('app') }} |
||||||
|
{% endblock %} |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
{% block body %}{% endblock %} |
||||||
|
</body> |
||||||
|
</html> |
Loading…
Reference in new issue