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ć!