1 Commits

Author SHA1 Message Date
Tim
5fc691c02a Create docker setup 2025-05-28 01:25:07 +02:00
38 changed files with 1135 additions and 729 deletions

View File

@ -0,0 +1,28 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html/public;
index index.php;
#client_max_body_size 100m;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php {
try_files $uri /index.php =404;
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
include fastcgi_params;
}
location ~ /\.(?:ht|git|svn) {
deny all;
}
}

36
.docker/php/Dockerfile Normal file
View File

@ -0,0 +1,36 @@
FROM php:8.4-fpm
ARG TIMEZONE
COPY php.ini /usr/local/etc/php/conf.d/docker-php-config.ini
RUN apt-get update && apt-get install -y \
gnupg \
g++ \
procps \
openssl \
git \
unzip \
zlib1g-dev \
libzip-dev \
libfreetype6-dev \
libpng-dev \
libjpeg-dev \
libicu-dev \
libonig-dev \
libxslt1-dev \
acl \
&& echo 'alias sf="php bin/console"' >> ~/.bashrc
RUN docker-php-ext-configure gd --with-jpeg --with-freetype
RUN docker-php-ext-install \
pdo pdo_mysql zip xsl gd intl opcache exif mbstring
# Set timezone
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone \
&& printf '[PHP]\ndate.timezone = "%s"\n', ${TIMEZONE} > /usr/local/etc/php/conf.d/tzone.ini \
&& "date"
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
WORKDIR /var/www/html

13
.docker/php/php.ini Normal file
View File

@ -0,0 +1,13 @@
memory_limit=1024M
opcache.enable=1
opcache.revalidate_freq=10
opcache.validate_timestamps=1
opcache.max_accelerated_files=10000
opcache.memory_consumption=192
opcache.max_wasted_percentage=10
opcache.interned_strings_buffer=1
opcache.fast_shutdown=1
upload_max_filesize = 20M
post_max_size = 20M

View File

@ -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
View File

@ -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 ###

View File

@ -8,20 +8,19 @@
"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/asset": "7.2.*",
"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": "*",
@ -85,7 +84,7 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.3.*"
"require": "7.2.*"
}
}
}

1103
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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:

View File

@ -1,3 +0,0 @@
framework:
property_info:
with_constructor_extractor: true

View File

@ -8,6 +8,4 @@ when@dev:
when@test:
framework:
profiler:
collect: false
collect_serializer_data: true
profiler: { collect: false }

View File

@ -1,4 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View File

@ -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

View File

@ -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'

37
docker-compose.yaml Normal file
View File

@ -0,0 +1,37 @@
services:
nginx:
image: nginx:stable
ports:
- "8080:80"
volumes:
- .:/var/www/html
- ./.docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
php:
build:
context: ./.docker/php
depends_on:
- db
volumes:
- .:/var/www/html
working_dir: /var/www/html
environment:
DATABASE_URL: mysql://user:password@db:3306/app?serverVersion=11.7.2-MariaDB
APP_ENV: prod
db:
image: mariadb:11.7.2
environment:
MARIADB_ROOT_PASSWORD: password
MARIADB_DATABASE: app
MARIADB_USER: user
MARIADB_PASSWORD: password
volumes:
- db_data:/var/lib/mysql
ports:
- "3306"
volumes:
db_data:

View File

@ -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);
}

View File

@ -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,28 +49,27 @@ 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(),
],
]);
}

View File

@ -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,7 +30,7 @@ class SnipContentController extends AbstractController
);
return $this->render('content/compare.html.twig', [
'snip' => $to->snip,
'snip' => $to->getSnip(),
'diff' => $diff,
]);
}

View File

@ -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()]);
}
}

View File

@ -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()]);
}
}

View File

@ -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;
}
}

View File

@ -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,
];
}
}

View File

@ -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);
}
}
}

View File

@ -10,22 +10,46 @@ 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;
}

View File

@ -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)) {

View File

@ -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;
}
}

View File

@ -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)) {

View File

@ -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);

View File

@ -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;

View File

@ -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,52 +45,52 @@ 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();
}

View File

@ -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) {

View File

@ -36,7 +36,7 @@ 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);
}

View File

@ -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);

View File

@ -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

View File

@ -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);
}
}

View File

@ -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": {
@ -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",
@ -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": {
@ -199,12 +177,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",

View File

@ -1,7 +0,0 @@
{% extends 'base/one.column.html.twig' %}
{% set title = 'Create Snip' %}
{% block body %}
{{ form(form) }}
{% endblock %}

View File

@ -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 %}