Návrhový vzor pre vlastnú triedu

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
iNviNho
Člen | 352
+
0
-

Ahojte, prechádzam akousi očistou a chcel by som sa s vami poradiť a nájsť nejaké spoločné riešenie pre to ako navrhovať triedy a do akej čistoty chodiť tak aby sme programovali rozšíriteľný kod, dodržiavali SRP a pod. Zisťujem že počas tých rokov sa moje zmýšlanie a návrhové vzory niekoľkokrát zmenili a chcem si to teda ujasniť.

Budem možno troška narážať a rozširovať tento thread


Dajme tomu, že si chcem naprogramovať triedu Article. Skúsim teda načrtnúť pár možností ako to môže byť

Možnosť 1

<?php

namespace App\Services;

/**
 * Description of Log
 *
 * @author Vladino
 */
class Article extends Nette\Object {

    /** @var \App\Repositories\ArticleRepository */
    public $articleRepository;

    public function __construct(\App\Repositories\ArticleRepository $articleRepository) {
	$this->articleRepository = $articleRepository;
    }

    /**
     * Insert new article
     * @param array $data
     */
    public function insertArticle($data) {
	$this->articleRepository->insertArticle($data);
    }

    /**
     * Updates article with given ID
     * @param array $data
     * @param int $id_art
     */
    public function updateArticle($data, $id_art) {
	$this->articleRepository->updateArticle($data, $id_art);
    }

    ...

}

?>

Toto riešenie je asi najjednoduchšie, ale aj najprimitívnejšie. Trieda slúži iba ako obálka a preprávka dát. Vkladanie dát trieda nerieši, ale predá túto akciu $articleRepository .

Možnosť 2

<?php

namespace App\Services;

/**
 * Description of Log
 *
 * @author Vladino
 */
class Article extends Nette\Object {

    public $id;

    public $title;

    public $description;

    /** @var \App\Repositories\ArticleRepository */
    public $articleRepository;

    public function __construct(\App\Repositories\ArticleRepository $articleRepository) {
	$this->articleRepository = $articleRepository;
    }

    /**
     * Inserts current article into database
     */
    public function insert() {

	$data = array(
	    "title" => $this->title,
	    "description" => $this->description
	);

	$this->articleRepository->insert($data);

    }

    /**
     * Updates current article
     */
    public function update() {

	$data = array(
	    "title" => $this->title,
	    "description" => $this->description
	);

	$this->articleRepository->update($data, $this->id);
    }

    public function setId($id) {
	$this->id = $id;
	return $this;
    }
    /* a ostatné settery ... */

}

?>

Toto riešenie je už viac objektové ako Možnosť 1, avšak ešte stále sa mi nepáči. Najväčší problém mám s konštruktorom, kedže rád by som ho mal takto:

<?php
public function __construct($id, \App\Repositories\ArticleRepository $articleRepository) {
	$this->articleRepository = $articleRepository;

	$data = $this->articleRepository->getArticleById($id);
	$this->title = $data["title"];
	$this->description = $data["description"];

    }
?>

Avšak tento konštruktor mi nedovolí vytvoriť objekt ak nepoznám ID (napr pri vkladaní article)… Dalo by sa to obísť ak by som ho dal ako NULL, avšak to sa mi moc nepáči.

Mám ešte pár otázok, ktoré mi stále lietajú hlavou ked premýšlam ako to navrhnúť:

  1. Ked má mať trieda napríklad 10 premenných tak ako ich má aj databáza, nie je lepšie mať radšej jednu premennú $data a v nej všetky data? Kedže väčšinu času len preposielame formulárom odoslané položky do DB.
  2. Môže mať trieda article metodu getArticleById($id) ked chcem nejako získať konkrétny článok? Alebo ju vždy vytvárať cez konštruktor a šablone predávať premenne objektu?

Viete mi trocha pomôcť, alebo nasmerovať, alebo preposlať vaše konkrétne riešenie ako by ste riešili vytváranie, editáciu, insert a delete Article?

Ďakujem.

newPOPE
Člen | 648
+
+1
-

Domnievam sa, ze to co si tu napisal je ze chces „Service“ (priklad 1) alebo ActiveRecord (priklad 2) (to ze on samotny chce Repository uz take dolezite nie je).

Cize otazka znie co vlastne chces? :) Skus si pozriet Doctrine2 architekturu a pripadne aj ine ORMs ako to cele riesia.

EDIT: To prve ve

Editoval newPOPE (24. 11. 2015 13:09)

Pavel Kravčík
Člen | 1205
+
+2
-

Myslím, že hledáš nějaké ORM. To už je prověřené, nastavené a plné best-practise.

Skvělé je toto: https://github.com/uestla/YetORM a nebo třeba LeanMapper, Doctrine

To čemu říkáš $data je nejlepší mít v obálce (entitě).
Metody save(), getById(), update() budou v repository.
A celé to bude v modelu, který může mít více repository.

Budeš mít např.

ArticleModel extends BaseModel
{
	protected $ArticleRepository;
	protected $TagsRepository;

	//construct DI načteš jak máš výše 2 repository

	public function articleSave(\Nette\Utils\ArrayHash $values) //data z formuláře
    {
		$articleEntity = new ArticleEntity();
		$articleEntity ->title = $values->title;
		$articleEntity ->text = $values->text;

		$id = $this->ArticleRepository->save($articleEntity);

		foreach($values->tags as $tag)
		{
			$tagEntity = new TagEntity();
			$tagEntity ->article_id = $id;
			$tagEntity ->title= $tag->title;

			$this->TagRepository->save($tagEntity);
		}
	}
}

Editoval Pavel Kravčík (24. 11. 2015 12:55)

iNviNho
Člen | 352
+
0
-

Aha aha zaujímavé. Lebo ja som chcel docieliť také niečo, že pre prácu s articlami mi bude stačiť Artcle trieda a article repository … ale pre vloženie articlu do DB, pre jeho napríklad načítanie do komponenty (teda jeho dat) by tá trieda robila strašne vela vecí …

Dalej mi niekedy nejde do hlavy načo napríklad by som písal

<?php
function setId($id) {
$this->id = $id;
}
?>

u article class ked vlastne bez updatu databáze mi to nie je moc platné.

Ako napríklad konkrétne by ste riešili úpravu už existujúceho articlu v kompomente? Predávali by ste mu vytvorené entitu article, alebo iba data?

Editoval iNviNho (24. 11. 2015 13:21)

Pavel Kravčík
Člen | 1205
+
0
-

Save() umí obojí. :) V tom je ta krása.

Já vlastně volám jen ->save($entity). To jestli to vložení nebo update už řeší repositář sám. Tj. žádné psaní. Tohle řeší entita a hrozně jednoduše:

namespace Dochazka\Entity;

/**
 * @property-read int $id
 * @property string $jmeno
 * @property string $prijmeni
 * @property int $skupina_id
 * @property int $pritomen
 * @property int $aktivni
 */
class ClovekEntity extends \App\Entity\BaseEntity
{
    const STAV_NEPRITOMEN = 0;
    const STAV_PRITOMEN = 1;
    const STAV_SLUZEBNI = 2;
}

Update vypadá následovně:

	$entity = $this->ClovekRepository->getById($id); //základní funkce repositáře getBy, findBy, save, delete, fetchPairs
	$entity->pritomen = 0; //lepší by volat tady tu konstantu - ale jsem línej to psát, tak radši napíšu dlouhej komentář :)
	$entity->setPritomen(0); //alternativa

	$this->ClovekRepository->save($entity);

Nemusíš nikam nic dopisovat, pokud entita reprezentuje tabulku.

Ako napríklad konkrétne by ste riešili úpravu už existujúceho articlu v kompomente? Predávali by ste mu vytvorené entitu article, alebo iba data?

Předávají se data z formuláře, jinak entita mezi funkcemi nebo ID.
Typický příklad je editační formulář. Ten Ti vrátí ArrayHash, to narveš do entity (mám funkci fillFromArray() nebo ručně viz výše) a pak už pracuješ s entitou, která hlídá co se kam dá napsat (string, int).

Editoval Pavel Kravčík (24. 11. 2015 13:27)

Felix
Nette Core | 1270
+
0
-

Pavel Kravčík napsal(a):

`php
namespace Dochazka\Entity;

/**
* @property-read int $id
* @property string $jmeno
* @property string $prijmeni
* @property int $skupina_id
* @property int $pritomen
* @property int $aktivni
*/
class ClovekEntity extends \App\Entity\BaseEntity
{
const STAV_NEPRITOMEN = 0;
const STAV_PRITOMEN = 1;
const STAV_SLUZEBNI = 2;
}

	$this->ClovekRepository->save($entity);
```

Hezke priklady. Ale ta cestina… Ta me trochu taha oci. :-)

Pavel Kravčík
Člen | 1205
+
-5
-

Felix napsal(a):

Hezke priklady. Ale ta cestina… Ta me trochu taha oci. :-)

Člověk si zvykne. Pracuji v jedné firmě v oboru pojišťovnictví. Jsou tu perly jako dobropis, prolongace, škodní průběh, vratka a další takové zavedené termíny. A prostě se to nedalo nějak rozumně přepsat do angličtiny → pak byla půlka česky a půlka anglicky. Je to dobrý i pro export do CSV co se tady hodně používá. Takže prostě jména objektů a sloupců jsou česky. Je to o zvyk. Tj. existují věci jako fakturaSave(), fakturaExport(), FakturaEdit().

Lepší systém mi nenapadá.

greeny
Člen | 405
+
+1
-

Je pravda, že ta česko-angličtina občas bývá problém, zvlášť u nějakých právnických či finančních systémů, kde je spousta termínů, které programátor málem nezná ani česky. Dost často se mi stává, že potřebuju nějakou věc a nevím která z těch X anglických konstant to je a musím si je překládat :)

iNviNho
Člen | 352
+
0
-

Dakujem velmi pekne za infosky a pomoc, skusim zajtra nabombit nieco a ked tak mi date vediet ci je to ok a spravny postup …

Len napr ak entita reprezentuje jednu tabulku, ako potom pracovat so selectami s joinnutymi tabulkami napriklad ak mam article a k nemu komentare? Bude to nova entita reprezentujuca ten join tabuliek? To mi pride ako hodne pisania

Šaman
Člen | 2668
+
0
-

Článek a komentáře jsou rozhodně jiné entity. Entity vycházejí z ER diagramu, nikoli ze struktury databáze.
Takže pokud budeš mít z nějakých důvodů třeba v jedné tabulce obecná data o nějaké smlouvě a podle typu smlouvy pak ve více tabulkách její doplňková data, tak je to jedna entita rozlezlá po více tabulkách. Ale článek-komentář je ukázkový příklad dvou entit a jejich vztahu (článek má komentáře).

iNviNho
Člen | 352
+
0
-

Ok, vytvoril som teda exemplárny príklad a ak sa vám dá, pozrite to prosím :)

Je to príklad na ukladanie multimédií a položil som ho na Kdyby\Doctrine2. Troška som to pochopil po svojom :)

Multimedia

<?php

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Multimedia
{

    use \Kdyby\Doctrine\Entities\MagicAccessors;

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id_mul;

    /**
     * @ORM\Column(type="string")
     */
    protected $name;

    /**
     * @ORM\Column(type="string")
     */
    protected $path;

    /**
     * @ORM\Column(type="string")
     */
    protected $type;

    /**
     * @ORM\Column(type="string")
     */
    protected $id_mul_fol;

    /**
     * @ORM\Column(type="datetime")
     */
    protected $datein;

    public function getId_mul() {
	return $this->id_mul;
    }

    public function getName() {
	return $this->name;
    }

    public function getPath() {
	return $this->path;
    }

    public function getType() {
	return $this->type;
    }

    public function getId_mul_fol() {
	return $this->id_mul_fol;
    }

    public function getDatein() {
	return $this->datein;
    }

    public function setId_mul($id_mul) {
	$this->id_mul = $id_mul;
    }

    public function setName($name) {
	$this->name = $name;
    }

    public function setPath($path) {
	$this->path = $path;
    }

    public function setType($type) {
	$this->type = $type;
    }

    public function setId_mul_fol($id_mul_fol) {
	$this->id_mul_fol = $id_mul_fol;
    }

    public function setDatein($datein) {
	$this->datein = $datein;
    }



}
?>

MultimediaService

<?php

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

namespace App\Services;

/**
 * Description of Multimedias
 *
 * @author Vladino
 */
class MultimediaService extends \Nette\Object
{
    private $em;

    public function __construct(\Kdyby\Doctrine\EntityManager $em)
    {
        $this->em = $em;
    }

    public function insert(\App\Entities\Multimedia $multimedia) {

	$this->em->persist($multimedia);
	$this->done();

    }

    public function update(\App\Entities\Multimedia $multimedia) {

	$this->em->persist($multimedia);
	$this->done();

    }

    public function delete(\App\Entities\Multimedia $multimedia) {

	$this->em->remove($multimedia);
	$this->done();
    }



    protected function done() {
	$this->em->flush();
    }

}
?>

MultimediaSaver

<?php

namespace App\Services;

use Nette\Utils\Image;
use Nette\Utils\Strings;

/**
 * Description of MultimediaSaver
 *
 * @author Vladino
 */
class MultimediaSaver {

    public $fileUpload;

    public $request;

    public $multimediaRepository;

    public function __construct(\Nette\Http\FileUpload $fileUpload, \Nette\Http\Request $request, \App\Repositories\MultimediaRepository $multimediaRepository) {

	$this->fileUpload = $fileUpload;
	$this->request = $request;
	$this->multimediaRepository = $multimediaRepository;

    }

    /**
     * Save fileUpload as image and returns Multimedia entity
     * @param type $path
     * @param type $x
     * @param type $y
     * @param type $newName
     * @param type $params
     * @return \App\Entities\Multimedia
     */
    public function saveAsImage($path, $x = "100%", $y = "100%", $newName = NULL, $params = NULL) {

	$image = Image::fromFile($this->fileUpload);

	$image->resize($x, $y, $params);

	if ($newName === NULL) {
	    $newName = $this->checkOriginalityOfName();
	}

	$imageWithPath = $path . $newName;

	$image->save($imageWithPath, 90);

	$multimedia = new \App\Entities\Multimedia;
	$multimedia->setName($newName);
	$multimedia->setType($this->fileUpload->getContentType());
	$multimedia->setPath($this->request->getUrl()->getBaseUrl() . $imageWithPath);
	$multimedia->setDatein(new \DateTime);

	return $multimedia;

    }

    /**
     * Checks if name of image is original, if not it will make it original
     * @return string $name
     */
    public function checkOriginalityOfName() {

	$multimediaNames = $this->multimediaRepository->getFetchPairedNames();
	$i = 1;

	$name = $this->fileUpload->getSanitizedName();

	while (in_array($name, $multimediaNames)) {
	    $name = $i.$name;
	    $i++;
	}

	return $name;

    }

   /**
    * Is image type GIF, PNG or JPEG?
    * @return bool
    */
    public function isImage() {
         return in_array($this->fileUpload->getContentType(), array('image/gif', 'image/png', 'image/jpeg'), TRUE);
    }

}

/**
 *
 * @author Vladino
 */
interface IMultimediaSaver {

    /**
    * @return \App\Services\MultimediaSaver
    */
    public function create(\Nette\Http\FileUpload $fileUpload);

}

?>

MultimediaRepository

<?php
namespace App\Repositories;

/**
 * Description of MultimediaRepository2
 *
 * @author Vladino
 */
class MultimediaRepository extends \Nette\Object {

    private $multimediaEntity;

    public function __construct(\Kdyby\Doctrine\EntityManager $em)
    {
	$this->multimediaEntity = $em->getRepository(\App\Entities\Multimedia::getClassName());
    }

    public function findById($id) {
	return $this->multimediaEntity->findOneBy(array("id_mul" => $id));
    }

    public function getFetchPairedNames() {
	return $this->multimediaEntity->findPairs(array(), "name");
    }

}
?>

No a konkrétny príklad ako to celé funguje je napríklad ked odošlem z formulára

  1. Cez interface vytvorím MultimediaSaver a predám u fileUpload
  2. Cez metodu saveAsImage vložím obrázok do defaultného adresára + táto metoda mi vráti entitu Multimedia
  3. Túto entitu vložím do DB cez MultimediaService
  4. Ked ju budem chcieť vytiahnuť použijem MultimediaRepository
<?php
$multimediaSaver = $this->iMultimediaSaver->create($v["file"]);
	$multimedia = $multimediaSaver->saveAsImage("multimedia/multimedias/");

	$this->multimediaService->insert($multimedia);
?>

Dobre som tomu pochopil?:) Dakujem za pomoc