Gorzkie Żale -
czyli słów kilka o smaczkach w projektach "greenfield"
Nie tylko SOLID. GRASP - jeszcze jeden sposób na kod wysokiej jakości"
18.15-19:00
Leszek Prabucki
-
Programuje od 2007
- Prowadzę Software House - Cocoders
- Współorganizuje PHPersów w Trójmieście i Toruniu
- Staram się myśleć (o architekturze kodu też)
Żal 1: Encje
<?php
declare(strict_types=1);
namespace App\Entity;
/**
* @Doctrine\ORM\Mapping\Table(name="article")
* @Doctrine\ORM\Mapping\Entity(repositoryClass="App\Repository\ArticleRepository")
* @Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity("slug")
* @Doctrine\ORM\Mapping\HasLifecycleCallbacks
*/
class Article
{
/**
* @var int
*
* @Doctrine\ORM\Mapping\Entity\Column(name="id", type="integer")
* @Doctrine\ORM\Mapping\Entity\Id
* @Doctrine\ORM\Mapping\Entity\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @Doctrine\ORM\Mapping\Entity\NotBlank
* @Doctrine\ORM\Mapping\Entity\Length(max=255)
* @Doctrine\ORM\Mapping\Entity\Column(name="title", type="string", length=255)
*/
private $title;
Żal 1: Encje
/**
* @Gedmo\Mapping\Annotation\Slug(fields={"title"})
* @Doctrine\ORM\Mapping\Entity\Column(name="slug", type="string", length=255, unique=true)
*/
private $slug;
/**
* @Assert\NotBlank
* @Doctrine\ORM\Mapping\Entity\Column(name="content", type="text")
*/
private $content;
/**
* @Gedmo\Timestampable(on="create")
* @Doctrine\ORM\Mapping\Entity\Column(name="created", type="datetime")
*/
private $created;
/**
* @Doctrine\ORM\Mapping\Entity\Column(name="image", type="string", length=255)
*/
private $image;
/**
* @Doctrine\ORM\Mapping\Entity\ManyToMany(targetEntity="Category", inversedBy="articles", cascade={"persist"})
* @Doctrine\ORM\Mapping\Entity\JoinTable(
* name="article_category",
* joinColumns={@Doctrine\ORM\Mapping\Entity\JoinColumn(name="article_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={@Doctrine\ORM\Mapping\Entity\JoinColumn(name="category_id", referencedColumnName="id", onDelete="CASCADE")}
* )
* */
private $categories;
Żal 1: Encje
public function __construct()
{
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getCategories(): \Doctrine\Common\Collections\Collection
{
return $this->categories;
}
public function setCreated(\DateTime $created)
{
$this->created = $created;
}
public function getCreated(): ?\DateTime
{
return $this->created;
}
public function setSlug(string $slug): string
{
$this->slug = $slug;
}
public function getSlug(): string
{
return $this->slug;
}
Żal 1: Encje - Gdzie problem?
<?php
declare(strict_types=1);
namespace App\Tests\Functional;
use App\Entity\Category;
use App\Entity\Article;
class SomeTest extends \Symfony\Bundle\FrameworkBundle\Test\WebTestCase
{
private Article $article;
public function setUp(): void
{
parent::setUp();
self::bootKernel();
$em = self::$kernel->getContainer()->get('doctrine')->getManager();
$this->article = new Article();
// Zgaduj zgadula jakie settery wykonać żeby fixture był poprawny
$em->persist($this->article);
$em->flush();
}
}
Żal 1: Encje - Gdzie problem?
<?php
declare(strict_types=1);
namespace App\Tests\Functional;
use App\Entity\Category;
use App\Entity\Article;
class SomeTest extends \Symfony\Bundle\FrameworkBundle\Test\WebTestCase
{
// ..
public function testThatSomething(): void
{
$article = $this->em->getRepository(Article::class)->find(1);
// przecież można nie? Co może pójść nie tak? enkapsulacja WTF!
$article->getCategories()->add(new Category());
// ..
}
}
Żal 1: Encje - Gdzie problem?
<?php
declare(strict_types=1);
namespace App\Entity;
// ...
class Article
{
/**
* @var string
*
* @Gedmo\Mapping\Annotation\Slug(fields={"title"})
* @ORM\Column(name="slug", type="string", length=255, unique=true)
*/
private $slug;
public function setSlug(string $slug): void
{
$this->slug = $slug;
}
}
????
Żal 1: Encje - Propozycja
<?php
declare(strict_types=1);
namespace App\Tests\Functional;
class SomeTest extends \Symfony\Bundle\FrameworkBundle\Test\WebTestCase
{
public function setUp(): void
{
$clock = new MockedClock();
$clock->setCurrentTime(new DateTimeImmutable('2019-09-12 10:03'));
$slugger = new MockedSlugger();
$this->article = new Article(
ArticleId::generateId(),
'Browar Brodacz jest dobry',
'Jakiś cały długi tekst o tym że piwo z Browaru Brodacz jest dobre',
$clock,
$slugger
);
$em->persist($this->article);
$em->flush();
}
}
Żal 1: Encje - To też nie fajne
<?php
declare(strict_types=1);
namespace App\Tests\Functional;
class SomeTest extends \Symfony\Bundle\FrameworkBundle\Test\WebTestCase
{
public function setUp(): void
{
$this->article = new Article(
ArticleId::generateId(),
'Browar Brodacz jest dobry',
'Jakiś cały długi tekst o tym że piwo z Browaru Brodacz jest dobre',
$clock,
$slugger,
null,
null,
null,
null
'image.jpg'
);
// settery na dane które są OPCJONALNE
$this->setNewUploadedImage('image.jpg');
}
}
Żal2: HasLifecycleCallbacks i entity listenery
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getEntity();
$this->uploadFile($entity);
}
private function uploadFile($entity)
{
// upload only works for Product entities
if (!$entity instanceof Product) {
return;
}
$file = $entity->getBrochure();
// only upload new files
if ($file instanceof UploadedFile) {
$fileName = $this->uploader->upload($file);
$entity->setBrochure($fileName);
} elseif ($file instanceof File) {
// prevents the full file path being saved on updates
// as the path is set on the postLoad listener
$entity->setBrochure($file->getFilename());
}
}
Żal2: HasLifecycleCallbacks i entity listenery - propozycja
use App\Service\FileUploader;
use Symfony\Component\HttpFoundation\Request;
// ...
public function new(Request $request, FileUploader $fileUploader)
{
// ...
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $brochureFile */
$brochureFile = $form['brochure']->getData();
if ($brochureFile) {
$brochureFileName = $fileUploader->upload($brochureFile);
$product->setBrochureFilename($brochureFileName);
}
// ...
}
// ...
}
Żal 3: 1 model do wszystkiego
<?php
$qb = $articleRepository->createQueryBuilder('a');
if ($reqest->query->get('categoryIds', false)) {
$query
->innerJoin('a.categries', 'c')
->andWhere('c.id = :categoryId')
->setParameter(
'categoryIds',
$reqest->query->get('categoryId'),
)
;
}
$results = $qb->getQuery()->execute();
/** @var Serializer $serializer */
return $this->json(
$results,
[],
['groups' => ['list']]
);
Żal 3: 1 model do wszystkiego
<?php
declare(strict_types=1);
namespace App\Entity;
/**
* @Doctrine\ORM\Mapping\Table(name="article")
* @Doctrine\ORM\Mapping\Entity(repositoryClass="App\Repository\ArticleRepository")
* @Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity("slug")
* @Doctrine\ORM\Mapping\HasLifecycleCallbacks
*/
class Article
{
/**
* @var int
*
* @Doctrine\ORM\Mapping\Entity\Column(name="id", type="integer")
* @Doctrine\ORM\Mapping\Entity\Id
* @Doctrine\ORM\Mapping\Entity\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @Doctrine\ORM\Mapping\Entity\NotBlank
* @Doctrine\ORM\Mapping\Entity\Length(max=255)
* @Doctrine\ORM\Mapping\Entity\Column(name="title", type="string", length=255)
* @Groups({'list', 'other-group'})
*/
private $title;
Żal 3: 1 model do wszystkiego - propozycja
<?php
namespace App\Query\ArticlePerCategoryListQuery;
class Article implements \JsonSerializable
{
private string $name;
private string $id;
public function __construct(string $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'name' => $this->name
];
}
}
Żal 3: 1 model do wszystkiego - propozycja
<?php
use App\Query\ArticlePerCategoryListQuery as ArticlePerCategoryListQueryInterface;
class ArticlePerCategoryListQuery implements ArticlePerCategoryListQueryInterface
{
private ConnectionRegistry $connection;
public function __construct(ConnectionRegistry $connectionRegistry)
{
$this->connectionRegistry = $connectionRegistry;
}
public function fetchList(?int $categoryId): array
{
$con = $this->connectionRegistry->getConnection();
if ($categoryId) {
return $con->project(
'SELECT a.id, a.name FROM articles WHERE category_id = :id ORDER BY created DESC',
['id' => $categoryId],
function (array $result) {
return new App\Query\RestArticleListQuery\Article(
$result['id'],
$result['name']
);
}
);
}
// wszystkie
}
}
Żal 3: 1 model do wszystkiego - propozycja
<?php
$categoryId = $request->query->get('categoryId', null);
return $this->json(
$articleInCategoryQuery->fetchList($categoryId)
);
Żal 4: Brak testów
Żal 5: Kodzik na ASAPie bez planowania
Żal 6: Brak dokumentacji
Żal 6: Brak dokumentacji ADL i ADR
propozycja
# 3. Wprowadzamy Elastic Search
Date: 2018-03-01
## Status
Accepted
## Context
Jest potrzeba skutecznego i wydajnego wyszukiwania pełno tekstowego po dokumentach typu
PDF, wordowych oraz tekstowych.
## Decision
Wdrażamy bazę elastic search jako projekcje eventów z pluginem który pozwala
indeksowanie dokumentów
## Consequences
Przebudowa projekcji może trwać długo ponieważ musimy indeksować dokument,
ale zmiana pomoże nam dobrze wyszukiwać i spełnić potrzebe biznesową.
Utrudnione testowanie integracyjne i funkcjonalne.
Żal 6: Brak dokumentacji README z tym jak instalować czy postawić projekt oraz istotnymi informacjami
Żal 7: Vendor lock
Żal 8: Brak onboardingu i code review
Dzięki!
Trzeba się napić!
Gorzkie Żale
By Leszek Prabucki
Gorzkie Żale
- 905