Merge pull request 'feature/3-git-content-database' (#8) from feature/3-git-content-database into main
Reviewed-on: #8
This commit is contained in:
		
							
								
								
									
										2
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								.env
									
									
									
									
									
								
							| @@ -24,6 +24,6 @@ APP_SECRET=a617c2ab616c5688ff5b0e95ad646641 | |||||||
| # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml | # 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="sqlite:///%kernel.project_dir%/var/data.db" | ||||||
| # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&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" | # DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8" | ||||||
| ###< doctrine/doctrine-bundle ### | ###< doctrine/doctrine-bundle ### | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|     "minimum-stability": "stable", |     "minimum-stability": "stable", | ||||||
|     "prefer-stable": true, |     "prefer-stable": true, | ||||||
|     "require": { |     "require": { | ||||||
|         "php": ">=8.2", |         "php": ">=8.3", | ||||||
|         "ext-ctype": "*", |         "ext-ctype": "*", | ||||||
|         "ext-iconv": "*", |         "ext-iconv": "*", | ||||||
|         "czproject/git-php": "^4.1", |         "czproject/git-php": "^4.1", | ||||||
| @@ -21,6 +21,7 @@ | |||||||
|         "symfony/runtime": "7.0.*", |         "symfony/runtime": "7.0.*", | ||||||
|         "symfony/security-bundle": "7.0.*", |         "symfony/security-bundle": "7.0.*", | ||||||
|         "symfony/twig-bundle": "7.0.*", |         "symfony/twig-bundle": "7.0.*", | ||||||
|  |         "symfony/uid": "7.0.*", | ||||||
|         "symfony/validator": "7.0.*", |         "symfony/validator": "7.0.*", | ||||||
|         "symfony/yaml": "7.0.*", |         "symfony/yaml": "7.0.*", | ||||||
|         "twig/extra-bundle": "^2.12|^3.0", |         "twig/extra-bundle": "^2.12|^3.0", | ||||||
|   | |||||||
							
								
								
									
										162
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										162
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | |||||||
|         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", |         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | ||||||
|         "This file is @generated automatically" |         "This file is @generated automatically" | ||||||
|     ], |     ], | ||||||
|     "content-hash": "81c31aacf558fbbb42aaa93138c76e40", |     "content-hash": "0d9cbf5f6f95b13006484d60ccb4d67c", | ||||||
|     "packages": [ |     "packages": [ | ||||||
|         { |         { | ||||||
|             "name": "czproject/git-php", |             "name": "czproject/git-php", | ||||||
| @@ -4097,6 +4097,88 @@ | |||||||
|             ], |             ], | ||||||
|             "time": "2023-08-16T06:22:46+00:00" |             "time": "2023-08-16T06:22:46+00:00" | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |             "name": "symfony/polyfill-uuid", | ||||||
|  |             "version": "v1.28.0", | ||||||
|  |             "source": { | ||||||
|  |                 "type": "git", | ||||||
|  |                 "url": "https://github.com/symfony/polyfill-uuid.git", | ||||||
|  |                 "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e" | ||||||
|  |             }, | ||||||
|  |             "dist": { | ||||||
|  |                 "type": "zip", | ||||||
|  |                 "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/9c44518a5aff8da565c8a55dbe85d2769e6f630e", | ||||||
|  |                 "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e", | ||||||
|  |                 "shasum": "" | ||||||
|  |             }, | ||||||
|  |             "require": { | ||||||
|  |                 "php": ">=7.1" | ||||||
|  |             }, | ||||||
|  |             "provide": { | ||||||
|  |                 "ext-uuid": "*" | ||||||
|  |             }, | ||||||
|  |             "suggest": { | ||||||
|  |                 "ext-uuid": "For best performance" | ||||||
|  |             }, | ||||||
|  |             "type": "library", | ||||||
|  |             "extra": { | ||||||
|  |                 "branch-alias": { | ||||||
|  |                     "dev-main": "1.28-dev" | ||||||
|  |                 }, | ||||||
|  |                 "thanks": { | ||||||
|  |                     "name": "symfony/polyfill", | ||||||
|  |                     "url": "https://github.com/symfony/polyfill" | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "autoload": { | ||||||
|  |                 "files": [ | ||||||
|  |                     "bootstrap.php" | ||||||
|  |                 ], | ||||||
|  |                 "psr-4": { | ||||||
|  |                     "Symfony\\Polyfill\\Uuid\\": "" | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "notification-url": "https://packagist.org/downloads/", | ||||||
|  |             "license": [ | ||||||
|  |                 "MIT" | ||||||
|  |             ], | ||||||
|  |             "authors": [ | ||||||
|  |                 { | ||||||
|  |                     "name": "Grégoire Pineau", | ||||||
|  |                     "email": "lyrixx@lyrixx.info" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "name": "Symfony Community", | ||||||
|  |                     "homepage": "https://symfony.com/contributors" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "description": "Symfony polyfill for uuid functions", | ||||||
|  |             "homepage": "https://symfony.com", | ||||||
|  |             "keywords": [ | ||||||
|  |                 "compatibility", | ||||||
|  |                 "polyfill", | ||||||
|  |                 "portable", | ||||||
|  |                 "uuid" | ||||||
|  |             ], | ||||||
|  |             "support": { | ||||||
|  |                 "source": "https://github.com/symfony/polyfill-uuid/tree/v1.28.0" | ||||||
|  |             }, | ||||||
|  |             "funding": [ | ||||||
|  |                 { | ||||||
|  |                     "url": "https://symfony.com/sponsor", | ||||||
|  |                     "type": "custom" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "url": "https://github.com/fabpot", | ||||||
|  |                     "type": "github" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", | ||||||
|  |                     "type": "tidelift" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "time": "2023-01-26T09:26:14+00:00" | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|             "name": "symfony/property-access", |             "name": "symfony/property-access", | ||||||
|             "version": "v7.0.0", |             "version": "v7.0.0", | ||||||
| @@ -5265,6 +5347,80 @@ | |||||||
|             ], |             ], | ||||||
|             "time": "2023-11-26T15:16:53+00:00" |             "time": "2023-11-26T15:16:53+00:00" | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |             "name": "symfony/uid", | ||||||
|  |             "version": "v7.0.0", | ||||||
|  |             "source": { | ||||||
|  |                 "type": "git", | ||||||
|  |                 "url": "https://github.com/symfony/uid.git", | ||||||
|  |                 "reference": "9472fe6a4a2adcc9150106ebb9fde328828d312f" | ||||||
|  |             }, | ||||||
|  |             "dist": { | ||||||
|  |                 "type": "zip", | ||||||
|  |                 "url": "https://api.github.com/repos/symfony/uid/zipball/9472fe6a4a2adcc9150106ebb9fde328828d312f", | ||||||
|  |                 "reference": "9472fe6a4a2adcc9150106ebb9fde328828d312f", | ||||||
|  |                 "shasum": "" | ||||||
|  |             }, | ||||||
|  |             "require": { | ||||||
|  |                 "php": ">=8.2", | ||||||
|  |                 "symfony/polyfill-uuid": "^1.15" | ||||||
|  |             }, | ||||||
|  |             "require-dev": { | ||||||
|  |                 "symfony/console": "^6.4|^7.0" | ||||||
|  |             }, | ||||||
|  |             "type": "library", | ||||||
|  |             "autoload": { | ||||||
|  |                 "psr-4": { | ||||||
|  |                     "Symfony\\Component\\Uid\\": "" | ||||||
|  |                 }, | ||||||
|  |                 "exclude-from-classmap": [ | ||||||
|  |                     "/Tests/" | ||||||
|  |                 ] | ||||||
|  |             }, | ||||||
|  |             "notification-url": "https://packagist.org/downloads/", | ||||||
|  |             "license": [ | ||||||
|  |                 "MIT" | ||||||
|  |             ], | ||||||
|  |             "authors": [ | ||||||
|  |                 { | ||||||
|  |                     "name": "Grégoire Pineau", | ||||||
|  |                     "email": "lyrixx@lyrixx.info" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "name": "Nicolas Grekas", | ||||||
|  |                     "email": "p@tchwork.com" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "name": "Symfony Community", | ||||||
|  |                     "homepage": "https://symfony.com/contributors" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "description": "Provides an object-oriented API to generate and represent UIDs", | ||||||
|  |             "homepage": "https://symfony.com", | ||||||
|  |             "keywords": [ | ||||||
|  |                 "UID", | ||||||
|  |                 "ulid", | ||||||
|  |                 "uuid" | ||||||
|  |             ], | ||||||
|  |             "support": { | ||||||
|  |                 "source": "https://github.com/symfony/uid/tree/v7.0.0" | ||||||
|  |             }, | ||||||
|  |             "funding": [ | ||||||
|  |                 { | ||||||
|  |                     "url": "https://symfony.com/sponsor", | ||||||
|  |                     "type": "custom" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "url": "https://github.com/fabpot", | ||||||
|  |                     "type": "github" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", | ||||||
|  |                     "type": "tidelift" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "time": "2023-10-31T08:22:02+00:00" | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|             "name": "symfony/validator", |             "name": "symfony/validator", | ||||||
|             "version": "v7.0.0", |             "version": "v7.0.0", | ||||||
| @@ -6149,10 +6305,10 @@ | |||||||
|     "prefer-stable": true, |     "prefer-stable": true, | ||||||
|     "prefer-lowest": false, |     "prefer-lowest": false, | ||||||
|     "platform": { |     "platform": { | ||||||
|         "php": ">=8.2", |         "php": ">=8.3", | ||||||
|         "ext-ctype": "*", |         "ext-ctype": "*", | ||||||
|         "ext-iconv": "*" |         "ext-iconv": "*" | ||||||
|     }, |     }, | ||||||
|     "platform-dev": [], |     "platform-dev": [], | ||||||
|     "plugin-api-version": "2.6.0" |     "plugin-api-version": "2.3.0" | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								config/packages/uid.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								config/packages/uid.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | framework: | ||||||
|  |     uid: | ||||||
|  |         default_uuid_version: 7 | ||||||
|  |         time_based_uuid_version: 7 | ||||||
| @@ -4,6 +4,8 @@ | |||||||
| # Put parameters here that don't need to change on each machine where the app is deployed | # 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 | # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration | ||||||
| parameters: | parameters: | ||||||
|  |     snipStorageType: 'db' # 'db' or 'git | ||||||
|  |     gitStoragePath: '%kernel.project_dir%/var/snips' | ||||||
|  |  | ||||||
| services: | services: | ||||||
|     # default configuration for services in *this* file |     # default configuration for services in *this* file | ||||||
| @@ -26,4 +28,5 @@ services: | |||||||
|  |  | ||||||
|     App\Service\SnipServiceFactory: |     App\Service\SnipServiceFactory: | ||||||
|         arguments: |         arguments: | ||||||
|             - '%kernel.project_dir%/var/snips' |             $gitStoragePath: '%gitStoragePath%' | ||||||
|  |             $storageType: '%snipStorageType%' | ||||||
							
								
								
									
										37
									
								
								migrations/Version20231217002445.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								migrations/Version20231217002445.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | <?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 Version20231217002445 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 snip_content (id BINARY(16) NOT NULL COMMENT \'(DC2Type:ulid)\', snip_id INT NOT NULL, parent_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:ulid)\', text LONGTEXT DEFAULT NULL, INDEX IDX_185DCA87140FD260 (snip_id), INDEX IDX_185DCA87727ACA70 (parent_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); | ||||||
|  |         $this->addSql('ALTER TABLE snip_content ADD CONSTRAINT FK_185DCA87140FD260 FOREIGN KEY (snip_id) REFERENCES snip (id)'); | ||||||
|  |         $this->addSql('ALTER TABLE snip_content ADD CONSTRAINT FK_185DCA87727ACA70 FOREIGN KEY (parent_id) REFERENCES snip_content (id)'); | ||||||
|  |         $this->addSql('ALTER TABLE user CHANGE roles roles JSON NOT NULL COMMENT \'(DC2Type:json)\''); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function down(Schema $schema): void | ||||||
|  |     { | ||||||
|  |         // this down() migration is auto-generated, please modify it to your needs | ||||||
|  |         $this->addSql('ALTER TABLE snip_content DROP FOREIGN KEY FK_185DCA87140FD260'); | ||||||
|  |         $this->addSql('ALTER TABLE snip_content DROP FOREIGN KEY FK_185DCA87727ACA70'); | ||||||
|  |         $this->addSql('DROP TABLE snip_content'); | ||||||
|  |         $this->addSql('ALTER TABLE `user` CHANGE roles roles LONGTEXT NOT NULL COLLATE `utf8mb4_bin`'); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -14,28 +14,28 @@ class HistoryController extends AbstractController | |||||||
| { | { | ||||||
|     public function __construct( |     public function __construct( | ||||||
|         private readonly SnipServiceFactory $snipServiceFactory, |         private readonly SnipServiceFactory $snipServiceFactory, | ||||||
|     ) |     ) {} | ||||||
|     { |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     #[Route('/', name: '_index')] |     #[Route('/', name: '_index')] | ||||||
|     public function index(Snip $snip): Response |     public function index(Snip $snip): Response | ||||||
|     { |     { | ||||||
|         $this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip); |         $this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip); | ||||||
|  |  | ||||||
|  |         $snipService = $this->snipServiceFactory->create($snip); | ||||||
|         return $this->render('history/index.html.twig', [ |         return $this->render('history/index.html.twig', [ | ||||||
|             'snip' => $snip, |             'snip' => $snip, | ||||||
|             'commits' => $this->snipServiceFactory->create($snip)->getRepo()->getAllCommits(), |             'versions' => $snipService->getVersions(), | ||||||
|  |             'latestVersion' => $snipService->getLatestVersion(), | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[Route('/set/{commit}', name: '_set')] |     #[Route('/set/{version}', name: '_set')] | ||||||
|     public function set(Snip $snip, string $commit): Response |     public function set(Snip $snip, string $version): Response | ||||||
|     { |     { | ||||||
|         $this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip); |         $this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip); | ||||||
|  |  | ||||||
|         $this->snipServiceFactory->create($snip)->getRepo()->checkout($commit); |         $this->snipServiceFactory->create($snip)->setVersion($version); | ||||||
|         $this->addFlash('success', 'Snip version set to ' . $commit); |         $this->addFlash('success', 'Snip version set to ' . $version); | ||||||
|         return $this->redirectToRoute('snip_single', ['snip' => $snip->getId()]); |         return $this->redirectToRoute('snip_single', ['snip' => $snip->getId()]); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -49,10 +49,11 @@ class SnipController extends AbstractController | |||||||
|         $this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip); |         $this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip); | ||||||
|  |  | ||||||
|         $snipService = $this->snipServiceFactory->create($snip); |         $snipService = $this->snipServiceFactory->create($snip); | ||||||
|  |         dump($snipService); | ||||||
|         return $this->render('snip/single.html.twig', [ |         return $this->render('snip/single.html.twig', [ | ||||||
|             'snip' => $snip, |             'snip' => $snip, | ||||||
|             'content' => $pl->parse($snipService->get()), |             'content' => $pl->parse($snipService->get()), | ||||||
|             'branch' => $snipService->getRepo()->getCurrentBranchName(), |             'branch' => $snipService->getCommit(), | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -126,7 +127,7 @@ class SnipController extends AbstractController | |||||||
|         $form = $this->createForm(ConfirmationType::class); |         $form = $this->createForm(ConfirmationType::class); | ||||||
|         $form->handleRequest($request); |         $form->handleRequest($request); | ||||||
|         if ($form->isSubmitted() && $form->isValid()) { |         if ($form->isSubmitted() && $form->isValid()) { | ||||||
|             $this->snipServiceFactory->create($snip)->deleteRepo(); |             $this->snipServiceFactory->create($snip)->delete(); | ||||||
|             $this->repository->remove($snip); |             $this->repository->remove($snip); | ||||||
|             $this->addFlash('success', sprintf('Snip "%s" deleted', $snip)); |             $this->addFlash('success', sprintf('Snip "%s" deleted', $snip)); | ||||||
|             return $this->redirectToRoute('snip_index'); |             return $this->redirectToRoute('snip_index'); | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ namespace App\Entity; | |||||||
|  |  | ||||||
| use App\Entity\Helpers\TrackedTrait; | use App\Entity\Helpers\TrackedTrait; | ||||||
| use App\Repository\SnipRepository; | use App\Repository\SnipRepository; | ||||||
|  | use Doctrine\Common\Collections\ArrayCollection; | ||||||
|  | use Doctrine\Common\Collections\Collection; | ||||||
| use Doctrine\ORM\Mapping as ORM; | use Doctrine\ORM\Mapping as ORM; | ||||||
|  |  | ||||||
| #[ORM\Entity(repositoryClass: SnipRepository::class)] | #[ORM\Entity(repositoryClass: SnipRepository::class)] | ||||||
| @@ -22,6 +24,17 @@ class Snip | |||||||
|     #[ORM\Column] |     #[ORM\Column] | ||||||
|     private ?bool $public = null; |     private ?bool $public = null; | ||||||
|  |  | ||||||
|  |     #[ORM\OneToMany(mappedBy: 'snip', targetEntity: SnipContent::class, orphanRemoval: true)] | ||||||
|  |     private Collection $snipContents; | ||||||
|  |  | ||||||
|  |     #[ORM\Column(length: 255, nullable: true)] | ||||||
|  |     private ?string $activeCommit = null; | ||||||
|  |  | ||||||
|  |     public function __construct() | ||||||
|  |     { | ||||||
|  |         $this->snipContents = new ArrayCollection(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public function __toString(): string |     public function __toString(): string | ||||||
|     { |     { | ||||||
|         return $this->name ?? ''; |         return $this->name ?? ''; | ||||||
| @@ -55,4 +68,46 @@ class Snip | |||||||
|  |  | ||||||
|         return $this; |         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->setSnip($this); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function removeSnipContent(SnipContent $snipContent): self | ||||||
|  |     { | ||||||
|  |         if ($this->snipContents->removeElement($snipContent)) { | ||||||
|  |             // set the owning side to null (unless already changed) | ||||||
|  |             if ($snipContent->getSnip() === $this) { | ||||||
|  |                 $snipContent->setSnip(null); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function getActiveCommit(): ?string | ||||||
|  |     { | ||||||
|  |         return $this->activeCommit; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function setActiveCommit(?string $activeCommit): static | ||||||
|  |     { | ||||||
|  |         $this->activeCommit = $activeCommit; | ||||||
|  |  | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										110
									
								
								src/Entity/SnipContent.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/Entity/SnipContent.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\Entity; | ||||||
|  |  | ||||||
|  | use App\Repository\SnipContentRepository; | ||||||
|  | use Doctrine\Common\Collections\ArrayCollection; | ||||||
|  | use Doctrine\Common\Collections\Collection; | ||||||
|  | use Doctrine\DBAL\Types\Types; | ||||||
|  | use Doctrine\ORM\Mapping as ORM; | ||||||
|  | use Symfony\Bridge\Doctrine\Types\UlidType; | ||||||
|  | use Symfony\Component\Uid\Ulid; | ||||||
|  |  | ||||||
|  | #[ORM\Entity(repositoryClass: SnipContentRepository::class)] | ||||||
|  | class SnipContent | ||||||
|  | { | ||||||
|  |     #[ORM\Id] | ||||||
|  |     #[ORM\Column(type: UlidType::NAME, unique: true)] | ||||||
|  |     #[ORM\GeneratedValue(strategy: 'CUSTOM')] | ||||||
|  |     #[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')] | ||||||
|  |     private ?Ulid $id = null; | ||||||
|  |  | ||||||
|  |     #[ORM\ManyToOne(inversedBy: 'snipContents')] | ||||||
|  |     #[ORM\JoinColumn(nullable: false)] | ||||||
|  |     private ?Snip $snip = null; | ||||||
|  |  | ||||||
|  |     #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] | ||||||
|  |     private ?self $parent = null; | ||||||
|  |  | ||||||
|  |     #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] | ||||||
|  |     private Collection $children; | ||||||
|  |  | ||||||
|  |     #[ORM\Column(type: Types::TEXT, nullable: true)] | ||||||
|  |     private ?string $text = 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -8,7 +8,7 @@ use DateTime; | |||||||
| class CustomGitRepository extends GitRepository | class CustomGitRepository extends GitRepository | ||||||
| { | { | ||||||
|     /** |     /** | ||||||
|      * @return SimpleCommit[] |      * @return array<SimpleCommit> | ||||||
|      * @throws \CzProject\GitPhp\GitException |      * @throws \CzProject\GitPhp\GitException | ||||||
|      */ |      */ | ||||||
|     public function getAllCommits(): array |     public function getAllCommits(): array | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								src/Repository/SnipContentRepository.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/Repository/SnipContentRepository.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\Repository; | ||||||
|  |  | ||||||
|  | use App\Entity\SnipContent; | ||||||
|  | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | ||||||
|  | use Doctrine\Persistence\ManagerRegistry; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @extends ServiceEntityRepository<SnipContent> | ||||||
|  |  * | ||||||
|  |  * @method SnipContent|null find($id, $lockMode = null, $lockVersion = null) | ||||||
|  |  * @method SnipContent|null findOneBy(array $criteria, array $orderBy = null) | ||||||
|  |  * @method SnipContent[]    findAll() | ||||||
|  |  * @method SnipContent[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) | ||||||
|  |  */ | ||||||
|  | class SnipContentRepository extends ServiceEntityRepository | ||||||
|  | { | ||||||
|  |     public function __construct(ManagerRegistry $registry) | ||||||
|  |     { | ||||||
|  |         parent::__construct($registry, SnipContent::class); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function save(SnipContent $entity, bool $flush = true): void | ||||||
|  |     { | ||||||
|  |         $this->getEntityManager()->persist($entity); | ||||||
|  |  | ||||||
|  |         if ($flush) { | ||||||
|  |             $this->getEntityManager()->flush(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function remove(SnipContent $entity, bool $flush = true): void | ||||||
|  |     { | ||||||
|  |         $this->getEntityManager()->remove($entity); | ||||||
|  |  | ||||||
|  |         if ($flush) { | ||||||
|  |             $this->getEntityManager()->flush(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | //    /** | ||||||
|  | //     * @return SnipContent[] Returns an array of SnipContent objects | ||||||
|  | //     */ | ||||||
|  | //    public function findByExampleField($value): array | ||||||
|  | //    { | ||||||
|  | //        return $this->createQueryBuilder('s') | ||||||
|  | //            ->andWhere('s.exampleField = :val') | ||||||
|  | //            ->setParameter('val', $value) | ||||||
|  | //            ->orderBy('s.id', 'ASC') | ||||||
|  | //            ->setMaxResults(10) | ||||||
|  | //            ->getQuery() | ||||||
|  | //            ->getResult() | ||||||
|  | //        ; | ||||||
|  | //    } | ||||||
|  |  | ||||||
|  | //    public function findOneBySomeField($value): ?SnipContent | ||||||
|  | //    { | ||||||
|  | //        return $this->createQueryBuilder('s') | ||||||
|  | //            ->andWhere('s.exampleField = :val') | ||||||
|  | //            ->setParameter('val', $value) | ||||||
|  | //            ->getQuery() | ||||||
|  | //            ->getOneOrNullResult() | ||||||
|  | //        ; | ||||||
|  | //    } | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								src/Service/SnipContent/SnipContentDB.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/Service/SnipContent/SnipContentDB.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\Service\SnipContent; | ||||||
|  |  | ||||||
|  | use App\Entity\Snip; | ||||||
|  | use App\Entity\SnipContent; | ||||||
|  | use App\Entity\User; | ||||||
|  | use Doctrine\ORM\EntityManagerInterface; | ||||||
|  |  | ||||||
|  | readonly class SnipContentDB implements SnipContentInterface | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private Snip                   $snip, | ||||||
|  |         private User                   $user, | ||||||
|  |         private EntityManagerInterface $em, | ||||||
|  |     ) {} | ||||||
|  |  | ||||||
|  |     public function update(string $snipContents): void | ||||||
|  |     { | ||||||
|  |         // Create new snipContent entity with previous one as parent | ||||||
|  |         $content = new SnipContent(); | ||||||
|  |         $content | ||||||
|  |             ->setText($snipContents) | ||||||
|  |             ->setSnip($this->snip) | ||||||
|  |         ; | ||||||
|  |         if ($this->snip->getSnipContents()->count() > 0) { | ||||||
|  |             $content->setParent($this->snip->getSnipContents()->last()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $this->em->persist($content); | ||||||
|  |         $this->em->flush(); | ||||||
|  |  | ||||||
|  |         $this->snip->setActiveCommit($content->getId()); | ||||||
|  |         $this->em->persist($this->snip); | ||||||
|  |         $this->em->flush(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function get(): string | ||||||
|  |     { | ||||||
|  |         $contentRepo = $this->em->getRepository(SnipContent::class); | ||||||
|  |         return $contentRepo->find($this->snip->getActiveCommit())->getText(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function getVersions(): array | ||||||
|  |     { | ||||||
|  |         // Return all snipContent entities (by parent) | ||||||
|  |         return array_map(fn(SnipContent $content) => [ | ||||||
|  |             'id' => (string)$content->getId(), | ||||||
|  |             'name' => $content->getId()->getDateTime()->format('Y-m-d H:i:s'), | ||||||
|  |         ], $this->snip->getSnipContents()->toArray()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function setVersion(string $version): void | ||||||
|  |     { | ||||||
|  |         $this->snip->setActiveCommit($version); | ||||||
|  |         $this->em->persist($this->snip); | ||||||
|  |         $this->em->flush(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function getCommit(): string | ||||||
|  |     { | ||||||
|  |         return $this->snip->getActiveCommit(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function delete(): void | ||||||
|  |     { | ||||||
|  |         // Cleanup the history | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function getLatestVersion(): string | ||||||
|  |     { | ||||||
|  |         return $this->snip->getSnipContents()->last()->getId(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,22 +1,21 @@ | |||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
| namespace App\Service; | namespace App\Service\SnipContent; | ||||||
| 
 | 
 | ||||||
| use App\Entity\User; | use App\Entity\User; | ||||||
| use App\Git\CustomGitRepository; | use App\Git\CustomGitRepository; | ||||||
|  | use App\Git\SimpleCommit; | ||||||
| use Symfony\Component\Security\Core\User\UserInterface; | use Symfony\Component\Security\Core\User\UserInterface; | ||||||
| 
 | 
 | ||||||
| class SnipService | class SnipContentGit implements SnipContentInterface | ||||||
| { | { | ||||||
|     private const SNIP_FILE_NAME = 'snip.txt'; |     private const string SNIP_FILE_NAME = 'snip.txt'; | ||||||
|     private const MASTER_BRANCH_NAME = 'master'; |     private const string MASTER_BRANCH_NAME = 'master'; | ||||||
| 
 | 
 | ||||||
|     public function __construct( |     public function __construct( | ||||||
|         private readonly CustomGitRepository $repo, |         private readonly CustomGitRepository $repo, | ||||||
|         private readonly ?User               $user, |         private readonly ?User               $user, | ||||||
|     ) |     ) {} | ||||||
|     { |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     private function snipExists(): bool |     private function snipExists(): bool | ||||||
|     { |     { | ||||||
| @@ -51,13 +50,31 @@ class SnipService | |||||||
|         return file_get_contents($this->getSnipPath()); |         return file_get_contents($this->getSnipPath()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function getRepo(): CustomGitRepository |     public function getVersions(): array | ||||||
|     { |     { | ||||||
|         return $this->repo; |         return array_map(fn(SimpleCommit $c) => [ | ||||||
|  |             'id' => $c->getHash(), | ||||||
|  |             'name' => $c->getDate()->format('Y-m-d H:i:s'), | ||||||
|  |         ], $this->repo->getAllCommits()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function deleteRepo(): void |     public function setVersion(string $version): void | ||||||
|  |     { | ||||||
|  |         $this->repo->checkout($version); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getCommit(): string | ||||||
|  |     { | ||||||
|  |         return $this->repo->getCurrentBranchName(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function delete(): void | ||||||
|     { |     { | ||||||
|         system("rm -rf " . escapeshellarg($this->repo->getRepositoryPath())); |         system("rm -rf " . escapeshellarg($this->repo->getRepositoryPath())); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public function getLatestVersion(): string | ||||||
|  |     { | ||||||
|  |         return self::MASTER_BRANCH_NAME; | ||||||
|  |     } | ||||||
| } | } | ||||||
							
								
								
									
										21
									
								
								src/Service/SnipContent/SnipContentInterface.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/Service/SnipContent/SnipContentInterface.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\Service\SnipContent; | ||||||
|  |  | ||||||
|  | interface SnipContentInterface | ||||||
|  | { | ||||||
|  |     public function update(string $snipContents): void; | ||||||
|  |  | ||||||
|  |     public function get(): string; | ||||||
|  |  | ||||||
|  |     /** @return array{id: string, name: string} */ | ||||||
|  |     public function getVersions(): array; | ||||||
|  |  | ||||||
|  |     public function setVersion(string $version): void; | ||||||
|  |  | ||||||
|  |     public function getCommit(): string; | ||||||
|  |  | ||||||
|  |     public function getLatestVersion(): string; | ||||||
|  |  | ||||||
|  |     public function delete(): void; | ||||||
|  | } | ||||||
| @@ -4,22 +4,34 @@ namespace App\Service; | |||||||
|  |  | ||||||
| use App\Entity\Snip; | use App\Entity\Snip; | ||||||
| use App\Git\CustomGit; | use App\Git\CustomGit; | ||||||
|  | use App\Service\SnipContent\SnipContentDB; | ||||||
|  | use App\Service\SnipContent\SnipContentGit; | ||||||
|  | use App\Service\SnipContent\SnipContentInterface; | ||||||
|  | use Doctrine\ORM\EntityManagerInterface; | ||||||
| use Symfony\Bundle\SecurityBundle\Security; | use Symfony\Bundle\SecurityBundle\Security; | ||||||
|  |  | ||||||
| class SnipServiceFactory | class SnipServiceFactory | ||||||
| { | { | ||||||
|  |  | ||||||
|     public function __construct( |     public function __construct( | ||||||
|         private readonly string $snipBasePath, |         private readonly string                 $gitStoragePath, | ||||||
|  |         private readonly string                 $storageType, | ||||||
|         private readonly Security               $security, |         private readonly Security               $security, | ||||||
|     ) |         private readonly EntityManagerInterface $em, | ||||||
|  |     ) {} | ||||||
|  |  | ||||||
|  |     public function create(Snip $snip): SnipContentInterface | ||||||
|     { |     { | ||||||
|  |         return match ($this->storageType) { | ||||||
|  |             'git'   => $this->createGit($snip), | ||||||
|  |             'db'    => $this->createDB($snip), | ||||||
|  |             default => throw new \Exception('Unknown storage type'), | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public function create(Snip $snip): SnipService |     private function createGit(Snip $snip): SnipContentGit | ||||||
|     { |     { | ||||||
|         $git = new CustomGit(); |         $git = new CustomGit(); | ||||||
|         $repoPath = sprintf('%s/%s', $this->snipBasePath, $snip->getId()); |         $repoPath = sprintf('%s/%s', $this->gitStoragePath, $snip->getId()); | ||||||
|         if (!is_dir($repoPath)) { |         if (!is_dir($repoPath)) { | ||||||
|             $repo = $git->init($repoPath); |             $repo = $git->init($repoPath); | ||||||
|             touch(sprintf('%s/.gitignore', $repoPath)); |             touch(sprintf('%s/.gitignore', $repoPath)); | ||||||
| @@ -28,6 +40,11 @@ class SnipServiceFactory | |||||||
|         } else { |         } else { | ||||||
|             $repo = $git->open($repoPath); |             $repo = $git->open($repoPath); | ||||||
|         } |         } | ||||||
|         return new SnipService($repo, $this->security->getUser()); |         return new SnipContentGit($repo, $this->security->getUser()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private function createDB(Snip $snip): SnipContentDB | ||||||
|  |     { | ||||||
|  |         return new SnipContentDB($snip, $this->security->getUser(), $this->em); | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										12
									
								
								symfony.lock
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								symfony.lock
									
									
									
									
									
								
							| @@ -140,6 +140,18 @@ | |||||||
|             "templates/base.html.twig" |             "templates/base.html.twig" | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|  |     "symfony/uid": { | ||||||
|  |         "version": "7.0", | ||||||
|  |         "recipe": { | ||||||
|  |             "repo": "github.com/symfony/recipes", | ||||||
|  |             "branch": "main", | ||||||
|  |             "version": "6.2", | ||||||
|  |             "ref": "d294ad4add3e15d7eb1bae0221588ca89b38e558" | ||||||
|  |         }, | ||||||
|  |         "files": [ | ||||||
|  |             "config/packages/uid.yaml" | ||||||
|  |         ] | ||||||
|  |     }, | ||||||
|     "symfony/validator": { |     "symfony/validator": { | ||||||
|         "version": "6.2", |         "version": "6.2", | ||||||
|         "recipe": { |         "recipe": { | ||||||
|   | |||||||
| @@ -6,14 +6,14 @@ | |||||||
|     <a href="{{ path('snip_single', {snip: snip.id}) }}" class="btn btn-primary"> |     <a href="{{ path('snip_single', {snip: snip.id}) }}" class="btn btn-primary"> | ||||||
|         <i class="fa fa-arrow-left"></i> Back |         <i class="fa fa-arrow-left"></i> Back | ||||||
|     </a> |     </a> | ||||||
|     <a href="{{ path('history_set', {commit: 'master', snip: snip.id}) }}" class="btn btn-warning"> |     <a href="{{ path('history_set', {version: latestVersion, snip: snip.id}) }}" class="btn btn-warning"> | ||||||
|         <i class="fa fa-refresh"></i> Master |         <i class="fa fa-refresh"></i> Latest | ||||||
|     </a> |     </a> | ||||||
|     <br><br> |     <br><br> | ||||||
|     <div class="list-group"> |     <div class="list-group"> | ||||||
|         {% for commit in commits %} |         {% for version in versions %} | ||||||
|             <a class="list-group-item" href="{{ path('history_set', {commit: commit.hash, snip: snip.id}) }}"> |             <a class="list-group-item" href="{{ path('history_set', {version: version.id, snip: snip.id}) }}"> | ||||||
|                 {{ commit.date|date }} - {{ commit.hash }} |                 {{ version.name }} - {{ version.id }} | ||||||
|             </a> |             </a> | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|     </div> |     </div> | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ | |||||||
|             {{ snip }} <small class="text-muted">#{{ snip.id }}</small> |             {{ snip }} <small class="text-muted">#{{ snip.id }}</small> | ||||||
|         </h4> |         </h4> | ||||||
|         <div class="card-header"> |         <div class="card-header"> | ||||||
|             <p class="card-text">Current branch: {{ branch }}</p> |             <p class="card-text">Current version: {{ branch }}</p> | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-body"> |         <div class="card-body"> | ||||||
|             {{ content|raw }} |             {{ content|raw }} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user