Slide 1

Slide 1 text

Efekt motyla – czyli jak można pracować z systemami legacy w PHPie

Slide 2

Slide 2 text

Jestem Leszek „l3l0” Prabucki @l3l0 No cześć!

Slide 3

Slide 3 text

- Edward Lorenz - wrażliwość zachowania układów nieliniowych na niewielkie zmiany warunków początkowych Efekt motyla

Slide 4

Slide 4 text

Co powoduje efekt motyla w kodzie? - duża ilość linii kodu oraz warunków logicznych - logika biznesowa rozmyta w różnych warstaw aplikacji - kod nastawiony na modyfikacje - upośledzona abstrakcja - upośledzone DRY - brak testów

Slide 5

Slide 5 text

Jak żyć z kodem legacy?

Slide 6

Slide 6 text

Kiedy, jak i po co migrować?

Slide 7

Slide 7 text

- Migracja wszystkiego naraz (Cold Turkey) - Migracja projektu etapami (Chicken Little) Strategie migracji

Slide 8

Slide 8 text

From nerovivo https://www.flickr.com/photos/dominik99/384027019

Slide 9

Slide 9 text

From Kevin Dooley https://www.flickr.com/photos/pagedooley/3199296759

Slide 10

Slide 10 text

From Kevin Dooley https://www.flickr.com/photos/pagedooley/3199296759

Slide 11

Slide 11 text

1. Opakowywanie kodu legacy 2. Proxy na poziomie serwera HTTP 3. Modyfikacje kodu legacy (testy + refactoring) 4. Dodawanie nowego kodu oraz jego intergracja z legacy Chicken Little sposoby migracji

Slide 12

Slide 12 text

Slide 13

Slide 13 text

Proxy na poziomie serwera HTTP

Slide 14

Slide 14 text

Modyfikacje kodu „legacy”

Slide 15

Slide 15 text

$v) { $ins[$k] = portal::safe($v); } $errors = cPerson::checkErrors($ins); if (!$errors) { $ins['user'] = $this->u->getLogin(); $person = cPerson::add($ins); for ($i = 1; $i <= 10; $i++) { if ($_POST['trade' . $i] && $_POST['subtrade' . $i] && $_POST['time' . $i]) { $tins['trade'] = $_POST['trade' . $i]; $tins['subtrade'] = $_POST['subtrade' . $i]; $tins['time'] = $_POST['time' . $i]; $tins['user'] = $this->u->getId(); $person->addTrade($tins); } } if (is_uploaded_file($_FILES['cv']['tmp_name'])) { $name = portal::upload($_FILES['cv']); $person->addFile($name, cPerson::fileCv); } header("Location: /?a=managePersonDetail&id={$person->getId()}"); die(); } else { foreach ($errors as $k => $v) { $arr['error_' . $k] = i18n_const::getPersonAddErrors($v); } $arr['main_error'] = 'block'; } } return t::instance()->render_once('managePersonAdd/main', $arr); } } Modyfikacje kodu legacy

Slide 16

Slide 16 text

exec('DELETE FROM person_trade'); $connection->exec('DELETE FROM person'); $session = new \Behat\Mink\Session(new \Behat\Mink\Driver\GoutteDriver()); $this->mink = new \Behat\Mink\Mink(['default' => $session]); $this->mink->setDefaultSessionName('default'); } /** * @test */ function shouldAllowToDefineContactPersonInformation() { $session = $this->mink->getSession(); $session->visit('http://localhost/index.php?a=managePersonAdd'); $currentPage = $session->getPage(); $currentPage->fillField('name', 'Leszek'); $currentPage->fillField('surname', 'Prabucki'); $currentPage->attachFileToField('cv', __DIR__.'/pathtocv.pdf'); $currentPage->pressButton('Dodaj'); $currentPage = $session->getPage(); $assertSession = $this->mink->assertSession(); $assertSession->addressMatches('#a=managerPersonDetails#'); $this->assertEquals('Leszek', $currentPage->findField('name')->getValue()); $this->assertEquals('Prabucki', $currentPage->findField('surname')->getValue()); } } Modyfikacje kodu legacy - test first

Slide 17

Slide 17 text

validate($request)) { $errors['main_error'] = 'block'; $content = t::instance()->render_once('managePersonAdd/main', [ 'errors' => $errors ]); return new \Symfony\Component\HttpFoundation\Response($content); } $ins = $request->request->all(); $ins['user'] = $this->u->getLogin(); $person = cPerson::add($ins); if ($cv = $request->files->get('cv', false)) { $name = portal::upload($cv); $person->addFile($name, cPerson::fileCv); } return new \Symfony\Component\HttpFoundation\RedirectResponse( sprintf('/?a=managePersonDetail&id=%d', $person->getId()) ); } private function validate(\Symfony\Component\HttpFoundation\Request $request) { $errors = cPerson::checkErrors($request->request->all()); $errors = array_map(function ($error) { return i18n_const::getPersonAddErrors($error); }, $errors); return $errors; } } Modyfikacje kodu legacy

Slide 18

Slide 18 text

From Favi Santos https://www.flickr.com/photos/loveisnotavictorymarch/5913517072 Modyfikacje kodu legacy mogą powodować efekt motyla

Slide 19

Slide 19 text

Co robić? Jak najmniej zmian w starym kodzie!

Slide 20

Slide 20 text

Command Handler + Command Bus lub Event Dispatcher

Slide 21

Slide 21 text

handle( new \Cocoders\HRAgencyContactList\UseCase\DefineContactPersonInformation\Command( $request->request->get('name'), $request->request->get('surname'), $this->u->getId() ) ); header("Location: /?a=managePersonDetail&id={$person->getId()}"); die(); } else { //.. } } return t::instance()->render_once('managePersonAdd/main', $arr); } } Command Bus usage:

Slide 22

Slide 22 text

getService('tactician.commandbus'); } private static function getKernel() { if (self::kernel) { return self::$kernel; } $kernel = new LegacyKernel(); $request = Request::createFromGlobals(); $request->attributes->set('isLegacy', true); $kernel->boot(); $container = $kernel->getContainer(); $container->enterScope('request'); $container->get('request_stack')->push($request); $container->set('request', $request); self::$kernel = $kernel; return self::$kernel; } } Shared kernel:

Slide 23

Slide 23 text

catalogue = $catalogue; } public function execute( DefineContactPersonInformation\Command $command, DefineContactPersonInformation\Responder $responder ) { $id = ContactPersonInformation\Id::generate(); $contactPersonInformation = new ContactPersonInformation( $id, ContactPersonInformation\FullName::fromFirstAndSurName( $command->getFirstName(), $command->getSurName() ), $command->getCreatorId() ); $this->catalogue->add($contactPersonInformation); $responder->contactPersonInformationDefined($id); } } Command handler:

Slide 24

Slide 24 text

Redundancja danych

Slide 25

Slide 25 text

Jak synchronizować dane ze „starą” bazą danych?

Slide 26

Slide 26 text

Anti-Corruption Layer

Slide 27

Slide 27 text

Repository pattern jako ACL

Slide 28

Slide 28 text

Slide 29

Slide 29 text

catalogue = $catalogue; } public function execute( DefineContactPersonInformation\Command $command, DefineContactPersonInformation\Responder $responder ) { $id = ContactPersonInformation\Id::generate(); $contactPersonInformation = new ContactPersonInformation( $id, ContactPersonInformation\FullName::fromFirstAndSurName( $command->getFirstName(), $command->getSurName() ), $command->getCreatorId() ); $this->catalogue->add($contactPersonInformation); $responder->contactPersonInformationDefined($id); } } Użycie repozytorium:

Slide 30

Slide 30 text

manager = $manager; } public function findCreatedBy(ContactPersonInformation\CreatorId $creatorId): array { return $this ->manager ->getRepository(ContactPersonInformation::class) ->findBy(['creatorId' => (string) $creatorId]) ; } public function add(ContactPersonInformation $contactPersonInformation) { $this->manager->persist($contactPersonInformation); } } Repo w Doctrine:

Slide 31

Slide 31 text

catalogue = $catalogue; $this->legacyConnection = $legacyConnection; } //... public function add(ContactPersonInformation $contactPersonInformation) { $maxLegacyPersonId = $this->legacyConnection->fetchColumn('SELECT max(id) FROM person'); $contactPersonInformationId = $contactPersonInformation->getId(); if ($contactPersonInformationId->isLegacy() && $contactPersonInformation->getId() > $maxLegacyPersonId) { throw new \LogicException('Cannot add new legacy strucutre'); } if ($contactPersonInformationId->isLegacy()) { $this->legacyConnection->update( 'person', [ 'name' => $contactPersonInformation->getFirstName(), 'surname' => $contactPersonInformation->getSurName() ], [ 'id' => (string) $contactPersonInformationId ] ); } $this->catalogue->add($contactPersonInformation); } Anti-Corruption Layer repo:

Slide 32

Slide 32 text

Podsumowanie

Slide 33

Slide 33 text

Dzięki! @l3l0