Včera zveřejnil Radek Hulán jednoduchý kód, který vytáhne data ze služby nabízené Evropskou Unií a vrátí je jako JSON. Podle svých slov se jedná o zjednodušený kód a tím pádem lze pochopit, že má jít o koncept, který může obsahovat chyby.
Zdrojový kód si můžete prohlédnout zde.
Na Radkovu kódu se mi nelíbí řada věcí, některé pochopitelné z důvodu jednoduchosti, druhé pro mě naprosto nepochopitelné.
Zkusil jsem je sepsat:
- návratová hodnota v proměnné $a? Proč? Mnohem lepší by bylo nějaké $responseData
- vracení stavu v poli stav v řetězci? V češtině? Myslím, že lepší by bylo vrátit error se zprávou (kvůli ladění) a errorCode, kde je číselný identifikátor chyby
- proč tolik magických konstant?
- proč chytání obecné Exception, když stačí konkrétní SoapFault?
K chybám, které i v jednoduchém kódu nemají co dělat, jsem přidal několik dalších úprav a aplikaci jsem postupně přepsal. Na Bitbucketu můžete vidět, jak jsem postupně kód přetvářel z původního na nový.
Cíle, které jsem si kladl jsou:
- testovatelný kód (při refaktoringu jsem udělal některé chyby proto, že jsem si testy nenapsal – ponaučení pro příště)
- čitelný kód
- oddělení MVC (a to tak, že pokud použiju vlastní request, response a controller, je funkcionalita použitelná v libovolném frameworku)
- použití Dependency Injection tam, kde to dává smysl
- oddělení stavu, kdy je odpověď OK a kdy je odpověď v nepořádku do samostatné logiky, ne jen přidat status do pole.
- znovupoužitelnost service
- pokud aplikace poroste, možnost šířit data napříč aplikací jako Company – objekt, který bude absorbovat práci s daty, nešířit aplikací pole (typické vyhnutí se Primitive Obsession, což je jeden z Code Smells)
Výsledkem je kód, který je sice delší, ale je daleko čitelnější, znovupoužitelnější a vhodný pro libovolný framework. Nehledě na to, že na něj jdou napsat testy (unit i integrační).
Výsledný kód:
<?php
class CompanyServiceException extends RuntimeException
{
const CODE_SERVICE_NOT_AVAILALE = 1;
const CODE_VAT_ID_UNKNOWN = 2;
}
class CompanyService
{
const SERVICE_WSDL_URL = 'http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl';
private function readFromSource($id, $country)
{
$soap = $this->createSoapClient();
$params = array('countryCode' => $country, 'vatNumber' => $id);
$result = $soap->checkVatApprox($params);
return $result;
}
protected function createSoapClient()
{
return new SoapClient(self::SERVICE_WSDL_URL);
}
private function createCompany($result, $country)
{
if ($this->isTraderAddressAndNotCity($result)) {
$t = explode("\n",$result->traderAddress);
$traderStreet = $t[0];
$traderCity = $t[1];
$traderPostcode = $t[2];
} else {
$traderStreet = $result->traderStreet;
$traderCity = $result->traderCity;
$traderPostcode = $result->traderPostcode;
}
return new Company($result->vatNumber, $country, $result->traderName, $traderStreet,
$traderCity, $traderPostcode);
}
private function isTraderAddressAndNotCity($result)
{
return !isset($result->traderCity) && isset($result->traderAddress);
}
function readByIdAndCountry($id, $country)
{
try {
$result = $this->readFromSource($id, $country);
if (! $result->valid) {
throw new CompanyServiceException("DIC {$country}{$id} nebylo nalezeno",
CompanyServiceException::CODE_VAT_ID_UNKNOWN, $e);
} else {
return $this->createCompany($result, $country);
}
} catch (SoapFault $e) {
throw new CompanyServiceException("EC neni dostupna",
CompanyServiceException::CODE_SERVICE_NOT_AVAILALE, $e);
}
}
}
class Company
{
private $id;
private $country;
private $firma;
private $ulice;
private $mesto;
private $psc;
function __construct($id, $country, $firma, $ulice, $mesto, $psc)
{
$this->id = $id;
$this->country = $country;
$this->firma = $firma;
$this->ulice = $ulice;
$this->mesto = $mesto;
$this->psc = $psc;
}
function toArray()
{
// dic = country . id
return array(
'ic' => $this->id,
'dic' => $this->country . $this->id,
'firma' => $this->firma,
'ulice' => $this->ulice,
'mesto' => $this->mesto,
'psc' => $this->psc,
);
}
}
class Request
{
private $data;
function __construct(Array $data = array())
{
$this->data = $data;
}
function __set($name, $value)
{
$this->data[$name] = $value;
}
function __get($name)
{
if (array_key_exists($name, $this->data)) {
return $this->data[$name];
}
}
}
class JsonResponse
{
private $data;
function __construct($data)
{
$this->data = $data;
}
function send()
{
header("Content-Type: application/json; charset=UTF-8");
echo json_encode($this->data);
}
}
class CompanyServiceController
{
private $companyService;
function __construct($companyService)
{
$this->companyService = $companyService;
}
function run($request)
{
try {
$company = $this->companyService->readByIdAndCountry($request->id, $request->country);
$responseData = $company->toArray();
} catch (CompanyServiceException $e) {
$responseData = array(
"error"=>$e->getMessage(),
"errorCode"=>$e->getCode(),
);
}
return new JsonResponse($responseData);
}
}
//$request = new Request($_REQUEST):
$request = new Request(array("id"=>"8501074769", "country"=>"CZ"));
$controller = new CompanyServiceController(new CompanyService);
$response = $controller->run($request);
$response->send();
