Archiwa tagu: Planeta PHP

Wpisy agregowane przez Planetę PHP

Phing, czyli zautomatyzuj swoje zadania #4

Kiedy projekt dochodzi do etapu, w którym trzeba/warto pokazać go „na zewnątrz” trzeba go jakoś wdrożyć na serwerze. Automatyzacja całego procesu budowania i wdrożenia jest ważna a wgranie plików na serwer jest jednym z etapów. W jego realizacji z wykorzystaniem FTP może pomóc Phing i zadanie FtpDeployTask. Żeby móc z niego skorzystać trzeba mieć w systemie pakiet PEAR: Net_FTP.

<ftpdeploy 
  host="${ftp.host}" 
  port="${ftp.port}" 
  username="${ftp.username}" 
  password="${ftp.password}"
  dir="${ftp.dir}" 
  passive="false"
  <fileset dir="dist">
    <include name="**"/>
  </fileset>
</ftpdeploy>

Jak widać powyżej użycie tego zadania jest banalnie proste (jeżeli jednak nie, to polecam moje wcześniejsze wpisy nt. Phinga).

Za dobry pomysł uważam budowanie projektu dla środowisk „zewnętrznych” w osobnym katalogu (ignorowanym przez IDE). Dzięki temu można uwzględnić strukturę katalogów na serwerze (np. umieszczenie plików wywoływanych przez cron w innym katalogu niż projekt). Dodatkową zaletą jest fakt, że mając odwzorowaną strukturę katalogów z serwera w podkatalogu można również wgrać pliki z wykorzystaniem klienta FTP. Przy mało stabilnym połączeniu internetowym wgranie plików projektu może być utrudnione ponieważ błąd wgrania jednego pliku przerywa wykonywanie całego zadania.

Przydatne linki:

Ku pamięci – .user.ini

Ze współdzielonymi hostingami bywa różnie – czasem nie udostępniają php.ini. Jednak w wielu przypadkach jest inna możliwość – chodzi o pliki .user.ini. Pozwalają one modyfikować działanie PHP w taki sam sposób jak poprzez php.ini.

PHP obsługuje tą metodę zmiany konfiguracji od wersji 5.3, a pliki można umieszczać w katalogu projektu (te pliki definiują konfigurację „dla katalogu”). Minusem tego rozwiązania jest ograniczony zakres zmian – tylko poziomy: PHP_INI_USER i PHP_INI_PERDIR (więcej).

Gdyby plik nie działał warto sprawdzić czy jego nazwa na danym serwerze nie została zmieniona. Nazwa jest zawarta w dyrektywie user_ini.filename, której wartość można sprawdzić na przykład poprzez phpinfo().

Przydatne linki:

Uruchomienie Symfony2 na home.pl

Ostatnio miałem okazję uruchamiać na hostingu współdzielonym od home.pl aplikację w Symfony2. Uruchomienie było testowe, ale jak zwykle były problemy.

Google podpowiedziało rozwiązanie umieszczone na forum PHP. Opis jest szczegółowy, jednak w moim wypadku efekt nie działał, więc zacząłem sam kombinować. Poniżej moje zapiski:

  1. utworzenie konta FTP z dostępem do katalogu /strona (oczywiście nazwy każdy dobiera indywidualnie);
  2. skierowanie domeny/subdomeny na katalog /strona/web, serwis musi być bez separacji;
  3. przegranie plików projektu do katalogu / konta FTP (tzn. do katalogu /strona całego konta hostingowego);
  4. dodanie pliku php.ini z poniższymi wpisami:
    short_open_tag = Off
    magic_quotes_gpc = Off
  5. w pliku web/.htaccess należy dodać komentarze w linijkach:
    RewriteCond %{ENV:REDIRECT_STATUS} ^$
    RewriteRule ^app\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
  6. jeżeli wersja PHP nie jest przełączona w panelu zarządzania hostingiem, trzeba to zrobić w pliku web/.htaccess dodając linijki:
    :Location /*.php
    Use php53 
    :Location

    minimum dla Symfony2 to PHP 5.3.3, ale home.pl udostępnia też PHP 5.4 (Use php54)

  7. po wyczyszczeniu cache i skonfigurowaniu aplikacja powinna już działać.

Aktualizacja [19.04.2014r]

Niestety uruchomienie Symfony2 na hostingu współdzielonym home.pl sprawia problemy. Problemy są o tyle dziwne, że są tymczasowe. Wygląda to tak, że po wgraniu aplikacja działa, po czasie zupełnie się „krzaczy” (przykład poniżej), następnie po czasie znów działa.

Fatal error: Uncaught exception 'InvalidArgumentException' with message 'The file "/config/routing.yml" does not exist.' in /panel/vendor/symfony/symfony/src/Symfony/Component/Config/FileLocator.php:48 Stack trace: 
#0 /panel/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Config/FileLocator.php(54): Symfony\Component\Config\FileLocator->locate('/config/routing...', NULL, true) 
#1 /panel/vendor/symfony/symfony/src/Symfony/Component/Routing/Loader/YamlFileLoader.php(49): Symfony\Component\HttpKernel\Config\FileLocator->locate('/config/routing...') 
#2 /panel/vendor/symfony/symfony/src/Symfony/Component/Config/Loader/DelegatingLoader.php(52): Symfony\Component\Routing\Loader\YamlFileLoader->load('/config/routing...', NULL) 
#3 /panel/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php(58): Symfony\Component\Config\Loader\DelegatingLoader->load('/config/routing...', NULL) 
#4 /panel/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php(54): Symfony\Bundle\Framewor in /panel/vendor/symfony/symfony/src/Symfony/Component/Config/FileLocator.php on line 48

Nie potrafię wyjaśnić przyczyny tego problemu, ale idąc za radą DOK usługodawcy zmieniłęm wersję PHP z 5.4 na 5.3 i od około miesiąca wszystko działa.

Może komuś ta informacja pomoże, chociaż dla mnie ten problem i jego specyfika jest dość niezrozumiała. Przywykłem do tego, że na danym serwerze aplikacja albo działa, albo nie. Natomiast w tym wypadku można powiedzieć, że „czasami”.

Phing, czyli zautomatyzuj swoje zadania #3

Trochę czasu mineło od ostatniego i w końcu udało mi się znaleźć chwilę na opisanie kolejnych elementów Phinga – filtrów i maperów.

Filtry

Filtry pozwalają na wykonywanie różnych operacji na zawartości plików – na przykład usuwanie komentarzy z kodu. Wywołanie filtrów grupuje się w tagu <filterchain>, przykład poniżej:

	<copy todir="kopia" overwrite="true">
		<fileset dir=".">
			<patternset refid="ogolne" />
		</fileset>
		<filterchain>
			<stripphpcomments />
		</filterchain>
	</copy>

Przykładowy kod definiuje operację kopiowania plików (zdefiniowanych w elemencie <patternset> o ID: „ogolne” do katalogu „kopia”. Oprócz zwykłego kopiowania pliki są przetwarzane przez łańcuch filtrów. W tym wypadku zawierający jeden filtr, który usuwa z plików komentarze PHP.

W momencie pisania tego posta Phing dostarczał 19 wbudowanych filtrów, listę można znaleźć w dokumentacji. Natomiast z popularniejszych filtrów myślę, że należy wymienić:

Z praktycznego punktu widzenia filtry są bardzo przydatne, ale mogą powodować pewne problemy w połączeniu z systemem kontroli wersji – o tym co można poradzić napiszę w dalszej części.

Mapery

Efektem działania filtrów jest zmiana zawartości plików, natomiast mapery działają analogicznie, ale z nazwami plików. W momencie przygotowywania wpisu Phing dostarczał 6 maperów. W moim odczuciu najbardziej przydatne są:

  • GlobMapper – podmiana nazw plików według prostych wzorców;
  • RegexpMapper – to samo, ale z wykorzystaniem wyrażeń regularnych we wzorcach.

Użycie maperów odbywa się w analogiczny sposób jak filtrów – w ramach jakiejś operacji plikowej (przykład zaczerpnięty z oficjalnej dokumentacji):

<copy todir="/tmp">
  <mapper type="glob" from="*.php" to="*.php.bak"/>
  <fileset refid="someid" />
</copy>

Praktyczny przykład użycia

Za dobry przykład użycia Phinga (filtry, mapery) i systemu kontroli wersji uważam generowanie plików konfiguracyjnych projektu.

Idea opiera się na plikach o rozszerzeniu .dist (kwestia umowna). Do systemu kontroli wersji trafiają pliki „szablonowe” (.dist), tak żeby ich struktura mogła być śledzona. Z drugiej strony, aby nie zawierały konkretnych informacji (np. dostęp do bazy danych). W tych plikach zamiast wartości pojawiają się Phingowe odwołania do zmiennych. Utworzenie działającego pliku konfiguracyjnego polega na:

  1. skopiowaniu pliku
  2. zmianie rozszerzenia z .php.dist na .php
  3. podstawieniu wartości w miejsce wywołań zmiennych

Plik config/Config.php.dist:

<?php

class Config
{
	const DB_HOST = '${config.db.host}';
	const DB_NAME = '${config.db.name}';
	const DB_USER = '${config.db.user}';
	const DB_PASSWORD = '${config.db.password}';
}

Plik srodowisko.properties:

config.db.host = localhost
config.db.name = site
config.db.user = siteuser
config.db.password = tAjNeHaSlO

Plik build.xml:

<?xml version="1.0" encoding="UTF-8"?>

<project name="HelloWorld" default="hello">
	<property file="srodowisko.properties" />

	<target name="configure">
		<copy todir="config">
			<fileset dir="config">
				<include name="*.php.dist" />
			</fileset>
			<mapper type="glob" from="*.php.dist" to="*.php" />
			<filterchain>
				<expandproperties />
			</filterchain>
		</copy>
	</target>

</project>

Po uruchomieniu komendy phing configure skrypt podstawi wartości z pliku właściwości, zmieni rozszerzenie i utworzy nowy plik, który można dołączyć do kodu projektu. Takie podejście daje dużą elastyczność, ponieważ cała konfiguracja środowiska może być zapisana w jednym pliku. Dzięki temu projekt można przekonfigurować czy to pod kątem środowiska produkcyjnego, czy developerskiego podstawiając jeden plik i wydając jedną komendę (a można to jeszcze uprościć). Zaletą jest również to, że w systemie kontroli wersji znajdują się pliki, bez wartości zależnych od środowiska. W tym celu, dla powyższego przypadku należy dodać do listy plików ignorowanych przez system kontroli wersji plik Config.php.

Przydatne linki:

Phing, czyli zautomatyzuj swoje zadania #2

Przyszedł czas na drugie spojrzenie na Phing – czym on jest napisałem w skrócie w poprzednim wpisie.

Parametry uruchomienia

To narzędzie konsolowe, więc jest uruchamiane za pomocą wywołania phing [opcje] [cel] w konsoli (najlepiej w katalogu zawierającym plik build.xml). W wywołaniu można umieścić parametry/opcje:

  • [nazwa celu] – nazwa celu, który ma zostać wywołany; jeżeli się nie pojawi zostanie uruchomiony cel domyślny projektu (<project ... default="domyslny_cel"/>);
  • -h / -help – wyświetlenie ogólnej pomocy aplikacji;
  • -l / -list – wyświetlenie listy celów zdefiniowanych w build.xml;
  • -f [plik] / -buildfile [plik] – nazwa pliku skryptu, jeżeli inna niż build.xml;
  • -D[właściwość]=[wartość] – przekazanie do skryptu właściwości o podanej wartości;
  • -propertyfile [plik] – wczytanie podanego pliku z deklaracjami właściwości;

Cele

W poprzedniej sekcji wspomniałem o parametrze pozwalającym wyświetlić listę dostępnych celów. Jednak nie każdy cel powinien być uruchamiany „samodzielnie” – czasem jest potrzebny tylko do spełnienia jakichś podstawowych wymagań dla innego celu, który wykonuje faktyczne operacje. Przykładem może być cel, który wczytuje/ustala odpowiednie właściwości konfiguracyjne (np. ścieżki do katalogów, dane FTP etc.), wywołanie takiego celu samodzielnie do niczego nie doprowadzi. Dlatego takie „prywatne” cele można ukryć na liście za pomocą atrybutu <target ... hidden="true"/>.

Kolejną i chyba nabardziej przydatną możliwością celów jest ustalanie dla nich zależności – na przykład, żeby wykonać cel „deploy” (który wgrywa pliki na serwer), należy najpierw te pliki/katalogi przygotować (np. usunąć pliki tymczasowe). W skrypcie Phing nie trzeba deklarować dodatkowo wywołań celów, od których zależy wykonanie bieżącego lub samemu wywoływać ręcznie cele w odpowiedniej kolejności. W tym celu wykorzystuje się atrybut <target ... depends="cel1,cel2,cel3"/>

Są jeszcze dwa pomocne atrybuty celów – if, unless. Pierwszy uruchamia cel tylko w momencie, kiedy zmienna o podanej nazwie jest zadeklarowana. Drugi ma działanie odwrotnie. Poniżej przykład:

<?xml version="1.0" encoding="UTF-8"?>

<project name="HelloWorld" default="hello">

    <property name="uruchomione" value="" />

    <target name="hello" depends="cel1,cel2">
        <echo>Hello</echo>
    </target>

    <target name="cel1" if="uruchomione">
        <echo>cel 1</echo>
    </target>

    <target name="cel2" unless="uruchomione">
        <echo>cel 2</echo>
    </target>

</project>

Poniżej wynik uruchomienia (pojawiają się nagłówki wszystkich wywołanych celów, ale kod cel2 nie został wykonany ze względu na warunek zawarty w atrybucie unless.

cim@cim-k52:~/public_html/Phing$ phing 
Buildfile: /home/cim/public_html/Phing/build.xml

HelloWorld > cel1:

     [echo] cel 1

HelloWorld > cel2:


HelloWorld > hello:

     [echo] Hello

BUILD FINISHED

Total time: 0.0581 seconds

Właściwości

Właściwości są swego rodzaju zmiennymi dla skryptu Phing i zostały już użyte w poprzednich przykładach. Warto pamiętać, że odwołania do właściwości mają budowę ${nazwaWlasciwosci}, np. <echo>Wartość zmiennej: ${nazwa}</echo>. Przydatne jest używanie kropek dla budowania hierarchii – ${aplikacja.baza.host}% – można w ten sposób grupować właściwości co mocno zwiększa czytelność.

Do tej pory w przykładach pojawiały się właściwości z wartościami zadeklarowanymi w pliku build.xml, jednak to nie jedyne możliwe źródła danych:

  • <property name="nazwa" value="wartosc" /> – pokazywany wcześniej sposób deklarowania zmiennej, w deklaracjach można odwoływać się do innych zmiennych (np. do jakiejś ścieżki dodać nazwe kolejnego katalogu);
  • <property file="plik.properties" /> – wczytuje do skryptu właściwości zdefiniowane w podanym pliku; można użyć również dodatkowego atrybutu <property file="plik.properties" prefix="zPliku" /> co spowoduje dodanie prefiksu do wszystkich nazw właściwości znajdujących się w pliku;
  • <echo>${env.PATH}</echo> – odwołania z prefiksem env. pozwalają na dostęp do zmiennych środowiskowych (zależne od systemu);

W przypadku zdublowania nazw właściwości Phing będzie zwracał pierwszą wartość właściwości, żeby zmienić to zachowanie trzeba dodać do drugiej deklaracji atrybut <property ... override="yes" />.

Podczas tworzenia ścieżek lub operacji plikowych pomocny może okazać się dostęp do ścieżki do katalogu projektu (zadeklarowanej w tagu <project ... basedir="/var/www/Projekt"> lub domyśnie ustawianej przez Phing). Ścieżka jest dostępna pod nazwą ${project.basedir}.

FileSet

Ponieważ Phing ma ułatwiać wdrożenia projektu posiada wsparcie do operowania na zbiorach plików – FileSet. Jeżeli na jakichś plikach mają zostać wykonane operacje można je wskazać bardzo dokładnie:

<target name="copy">
    <copy todir="kopia">
        <fileset dir=".">
            <exclude name="*.xml" />
            <include name="*.properties" />
            <include name="*.php" />
        </fileset>
    </copy>
</target>

Powyższa definicja przedstawia cel kopiujący pliki do katalogu „kopia” za pomocą polecenia copy. Natomiast miejsce z którego pliki są kopiowane („.” – czyli bieżący katalog) oraz deklaracje, które pliki (w tym wypadku według rozszerzeń) mają być kopiowane lub pomijane definiuje typ FileSet.

Definiowanie dołączeń/wykluczeń jest elastyczne:

  • można wykorzystywać atrybuty: <fileset ... includes="..."/> i <fileset ... excludes="..."/> z wykorzystaniem przecinków lub spacji podawać wzorce, jednak powyżej kilku wzorców ta metoda sprawia, że skrypt jest mniej czytelny;
  • można wykorzystać atrybuty: <fileset ... includesfile="..."/> i <fileset ... excludesfile="..."/> podając ścieżki do plików, w których każda linijka zawiera jeden wzorzec; jednak wymaga to utrzymywania dodatkowych plików oprócz niezbędnego build.xml;
  • można użyć tagów <include/> i <exclude/> w tagu FileSet tak jak w przykładzie powyżej; chociaż przy wielu operacjach powtarzanie tego samego zestawu dołączeń/wykluczeń i późniejsze ich utrzymywanie różnież może być kłopotliwe;
  • można użyć typu PatternSet.

Deklarowanie wzroców dopasowań nazw plików opiera się na gwiazkach („*” lub „**”). Jedna gwiazka pozwala na dopasowanie nazwy pliku/katalogu jednak w ramach tego dopasowania mogą się znajdować tylko pliki/katalogi bezpośrednio w danym katalogu. Natomiast dwie gwiazdki pozwalają na dopasowania również z uwzględnieniem zawartości podkatalogów. Przykłady z opisem są w oficjalnej dokumentacji.

PatternSet

W poprzenim akapicie wymieniłem kilka możliwości definiowania dołączeń/wykluczeń dla typu FileSet wraz z ich niedogodnościami. Ostatnią opcją jest typ PatternSet, który najprościej można opisać jako zbiór wzorców dla FileSet. Poniżej zmodyfikowany kod celu kopiującego pliki:

<patternset id="ogolne">
    <exclude name="*.xml" />
    <include name="*.properties" />
    <include name="*.php" />
</patternset>

<target name="copy">
    <copy todir="kopia">
        <fileset dir=".">
            <patternset refid="ogolne" />
        </fileset>
    </copy>
</target>

Wynik wykonania celu przedstawionego powyżej i z poprzedniego przykładu są identyczne. Różnia polega na tym, że wykorzystanie PatternSet ułatwia zapanowanie nad definicjami warunków – można je zdefiniować globalnie i tylko odwoływać się w razie potrzeby, a zmiany ograniczają się do jednego miejsca. Warto też zauważyć, że PatternSet w przeciwieństwie do FileSet jest niezależny od katalogu. Można zdefiniować warunki takie jak dołączanie plików PHP, HTML, TPL, pomijanie plików logów, plików tymczasowych etc. i używać ich w kontekście różnych katalogów. Sposoby definiowania warunków w PatternSet są takie same jak w FileSet z wyjątkiem możliwości odwoływania się do typu PatternSet.

Przydatne linki:

Phing, czyli zautomatyzuj swoje zadania #1

Pracując przy projekcie programistycznym, oprócz pisania kodu trzeba też m.in. zarządzać projektem – może to być uruchomienie testów, skonfigurowanie projektu czy wdrożenie projektu etc. Z reguły każde z takich zadań wymaga wykonania jakichś operacji, często wielu i w określonej kolejności – w tym miejscu „zapala się lampka”. Takie operacje można, a nawet trzeba, zautomatyzować! Dzięki temu uniknie się błędów wynikających z rutyny, znudzenia wykonywaniem ciągle tych samych operacji, bo czas można poświęcić na ciekawsze sprawy.

Dla programistów Javy znajomo brzmi nazwa Ant. Dla programistów PHP istnieje podobne narzędzie o nazwie Phing. W obu przypadkach, w skrócie, chodzi o możliwość „oprogramowania” różnych zadań związanych z projektem i późniejsze tylko uruchamianie odpowiednich procedur, które aplikacja zrobi za użytkownika.

Tym wpisem chciałbym rozpocząć cały cykl wpisów na temat narzędzia Phing, a ponieważ to pierwszy wpis zacznijmy od podstaw.

Instalacja

Instalacja narzędzia jest możliwa na kilka sposobów, wszystkie opisane są na tej stronie: http://www.phing.info/trac/wiki/Users/Installation. Poprawność instalacji, można zweryfikować poleceniem w konsoli:

root@cim-k52:~# phing -version
Phing 2.6.1

Użycie

Użycie Phinga do zarządzania projektem opiera się na utworzeniu i wypełnieniu pliku build.xml. Jak wskazuje rozszerzenie jest to plik XML. Dzięki temu nie trzeba się uczyć nowej składni (wystarczy znajomość podstaw XML). Dodatkowo działanie Phinga jest niezależne od systemu operacyjnego (włącznie ze ścieżkami do plików). Oprócz build.xml bardzo przydatne są pliki właściwości (*.properties), które są źródłem danych dla skryptu1.

Na początek kilka „słów kluczowych” odnośnie projektu z perspektywy Phing:

  • project (projekt) – <project> to główny element w pliku build.xml, tzw. root node; definuje projekt, jego nazwę, opis, domyślny cel; dokumentacja
  • target (cel) – <target> definiowane cele, które będzie można później wywoływać, np. konfiguracja projektu, wdrożenie na środowisko, uruchomienie testów; cele składają się z wywołań zadań; dokumentacja
  • properties (właściwość) – <property> pozwala definiować właściwości, które w grunie rzeczy oznaczają zmienne skryptu; dokumentacja
  • task (zadanie) – to konkretne zadania, których wykonanie składa się na osiągnięcie celu, np. skopiowanie/przeniesienie pliku, wgranie na serwer; w dokumentacji można znaleść listę dostępnych zadań

Na koniec pierwszego wpisu testowe uruchomienie, poniżej treść pliku build.xml:

<?xml version="1.0" encoding="UTF-8"?>

<project name="HelloWorld" default="hello">

    <property name="who"  value="World" />

    <target name="hello">
	<echo>Hello ${who}</echo>
    </target>

</project>

Plik build.xml najlepiej umieścić w głównym katalogu projektu. Podstawowym sposobem użycia jest wywołanie w katalogu zawierającym plik skryptu z poziomu konsoli:

cim@cim-k52:~/public_html/Phing$ phing hello
Buildfile: /home/cim/public_html/Phing/build.xml

HelloWorld > hello:

     [echo] Hello World

BUILD FINISHED

Total time: 0.0559 seconds

W powyższym przypadku wywołanie phing i phing hello daje ten sam efekt, ponieważ cel hello jest domyślnym celem projektu.

Komunikaty na konsoli pozwalają śledzić operacje (cele i zadania) wykonywane przez Phing (co jest szczególnie przydatne przy wywoływaniu celów, które zależą od osiągnięcia innych celów). Na koniec pojawia się komunikat o „BUILD FINISHED” informujący o poprawnym zakończeniu pracy, w innym wypadku pojawi się „BUILD FAILED” ze szczegółowym komunikatem błędu.


1 zawartość pliku build.xml będę nazywał skryptem, jeżeli macie/znacie lepsze nazwy piszcie w komentarzach

Zalecane standardy i dobre praktyki

Uważam, że PHP jest językiem bardzo „elastycznym”. Można napisać skrypt księgi gości (kto od takich rzeczy nie zaczynał? ;)) po przerobieniu tutoriala z internetu, można też napisać skomplikowaną i rozbudowaną aplikację internetową. W wszystko w jednym, tym samym języku.

Z jednej strony jest to zaleta – pozwala stosunkowo szybko osiągnąć działający efekt oraz pozwala się uczyć w obrębie jednego języka i rozwijać swoje umiejętności. Z drugiej strony jest to wada, bo pozwala pisać kod niechlujny, może nawet do tego zachęca/przyzwyczaja – skoro pierwsze efekty można było osiągnąć „metodą Copy’ego Paste’a”, to dlaczego kolejne mają wymagać więcej pracy?. W efekcie można spotkać dziwaczny kod, pełen funkcji zadeklarowanych tam gdzie akurat były potrzebne, zmiennych globalnych nagminnie używanych w metodach/funkcjach, kodu HTML, CSS, JS, PHP przemieszanego w obrębie jednego pliku etc. Obiektowość również się zdarza, ale budowa obiektów i ich wykorzystanie nie napawają optymizmem.

Myślę jednak, że nie należy winić języka. Jeżeli ktoś ma „głód wiedzy”, to taki kod będzie tworzył na początku, a później będzie się uczył, uczył i uczył jak programować coraz lepiej. Za cenną pomoc w tej nauce uważam dwie inicjatywy: PHP Framework Interop Group i PHP: The Right Way.

PHP Framework Interop Group

Jest to grupa ustalająca propozycje standardów dla kodu pisanego w języku PHP. Wypowiedzieć się w dyskusji może każdy chętny, jednak głosować mogą tylko przedstawiciele projektów (np. Zend Framework, Symfony2, Drupal, Doctrine, eZ Publish). Do tej pory grupa uzgodniła i zatwierdziła 4 standardy:

  • PSR-0, Autoloading Standard – opisuje wymogi, jakie musi spełniać autoloader, żeby mógł obsługiwać różne biblioteki;
  • PSR-1, Basic Coding Standard – opisuje ogólne wymogi kodu (kodowanie plików, tagi PHP, klasy, metody, właściwości, stałe);
  • PSR-2, Coding Style Guide – opisuje szczegółowe zalecenia formatowania kodu;
  • PSR-3, Logger Interface – opisuje interfejs dla loggerów, którego użycie umożliwi obsługę logów w różnych aplikacjach i bibliotekach z wykorzystaniem tego samego mechanizmu obsługi logów.

Nikt nie wymaga stosowania PSR-*, jednak uważam to za dobrą praktykę. Dodatkowo pisanie kodu przestrzegającego tych standardów może ułatwić jego późniejsze wykorzystanie jako bibliotek czy wykorzystanie w swoim projekcie zewnętrznych bibliotek.

Strona FIG znajduje się tutaj, jednak zawiera tylko zatwierdzone standardy. Natomiast propozycje kolejnych można znaleść na GitHubie grupy – tutaj.

PHP: The Right Way

Ten projekt jest zbiorem dobrych rad, kierunków rozwoju, narzędzi/technik którymi warto się zainteresować chcąc profesjonalnie programować w PHP. Dodatkowo zakres tematyczny jest bardzo szeroki – od pierwszych kroków, przez zarządzanie zależnościami (Composer, PEAR), po serwery, testowanie i cache. Dodatkowo strona jest przetłumaczona na język polski i dostępna tutaj.

Zmiany w certyfikacji Zend

Pierwsze wydania głównych (obecnie) gałęzi PHP miały miejsce:

  • 5.3.0 – 30 czerwca 2009r
  • 5.4.0 – 1 marca 2012r
  • 5.5.0 – 20 czerwca 2013r

Natomiast egzamin na certyfikat z PHP przez dłuższy czas dotyczył wersji 5.3, jednak nadchodzą zmiany.

Po pierwsze dotychczasowy egzamin (obejmujący PHP 5.3) będzie dostępny tylko do 31 grudnia 2013 roku. W jego miejsce pojawił się egzamin obejmujący PHP 5.5. Zmienia się również nazwa, teraz będzie to „Zend Certified PHP Developer”, co Zend tłumaczy dopasowaniem nazwy do innych certyfikatów IT. Według informacji ze strony rejestracja na nową wersję egzaminu była dostępna od 3 października, natomiast sam egzamin od dziś (7 października).

Nadal dostępny jest Study Guide dla wersji 5.3 a prace nad jego aktualizacją do 5.5 trwają i efekty mają być w przyszłym miesiącu (podejrzewam, że chodzi o listopad).

Źródło: PHP Certification

Typ wyliczeniowy – SplEnum i alternatywy

We wpisie Magiczne „cyferki” pisałem o używaniu stałych zamiast magicznych cyfr, które dla osoby nie znającej szczegółów projektu nic nie znaczą. Stałe zamiast cyfr są postępem, jednak pozostaje problem poprawności danych. Na przykład wartość 1 może mieć status obiektu firmy, osoby i wielu innych obiektów w aplikacji. W dodatku dla każdego obiektu może on oznaczać coś zupełnie innego. Z drugiej strony przy większej liczbie statusów w jednym obiekcie i mniejszej w innych można przez pomyłkę przypisać status innego obiektu, który jest poza zakresem danego obiektu.

Krótko mówiąc pozostaje problem ograniczenia możliwości ustawienia statusu tylko z określonego zakresu/typu. Typ jest tutaj istotną wskazówką – m.in. w Javie są tytułowe typy wyliczeniowe, które pozawalają zdefiniować nowy typ, dla określonego zbioru wartości, a język poprzez typowanie parametrów metody przypilnuje, żeby ustawiać statusy tylko dozwolonego typu.

W PHP również jest typowanie parametrów (tylko dla typów złożonych) i jak się okazuje w bibliotece SPL znajduje się nawet klasa dla typów wyliczeniowych – SplEnum. Jej budowa jest dość prosta a przykłady z manuala klarowne. Jest jednak jeden problem – nie jest standardowym elementem języka i wymaga doinstalowania rozszerzenia PECL na serwerze. Dla posiadaczy serwerów dedykowanych/VPS nie stanowi to problemu, jednak przy uruchamianiu aplikacji na hostingu współdzielonym zainstalowanie tego rozszerzenia zależy od administratora.

Jak wspominałem trochę wyżej budowa klasy jest dość prosta, więc zamiast zastanawiać się nad dostępnością/możliwością instalacji rozszerzenia na serwerze można napisać odpowiednik tej klasy samemu, poniżej przykładowa implementacja.

<?php

/**
 * My implementation of alternative SplEnum 
 * @link http://php.net/manual/en/class.splenum.php
 */
class MyEnum 
{
    /**
     * Value of object
     * @var mixed
     */
    private $value;
    
    /**
     * Name of const with default value
     * @var string
     */
    private $defaultName = '__default';
    
    /**
     * Reflection object
     * @var ReflectionClass
     */
    private $reflection;

    /**
     * Create object
     * @param mixed $initialValue
     * @throws UnexpectedValueException
     */
    public function __construct($initialValue = null)
    {
        if ($initialValue && in_array($initialValue, $this->getConstList())) {
            $this->value = $initialValue;
        } elseif (!$initialValue && $this->getDefault()) {
            $this->value = $this->getDefault();
        } else {
            throw new UnexpectedValueException('Value not a const in enum '. get_class($this));
        }
    }
    
    /**
     * Get array of class consts
     * @param boolean $includeDefault
     * @return array
     */
    public function getConstList($includeDefault = false)
    {
        $tmp = $this->getReflection()->getConstants();
        if (!$includeDefault && array_key_exists($this->defaultName, $tmp)) {
            unset($tmp[$this->defaultName]);
        }
        return $tmp;
    }
    
    /**
     * Get value of object
     * @return mixed
     */
    public function getValue()
    {
        return $this->value;
    }
    
    public function __toString()
    {
        return (string) $this->getValue();
    }
    
    /**
     * Get default value from class
     * @return mixed
     */
    protected function getDefault()
    {
        return $this->getReflection()->getConstant($this->defaultName);
    }

    /**
     * Get instance of class reflection
     * @return ReflectionClass
     */
    protected function getReflection()
    {
        if (!$this->reflection) {
            $this->reflection = new ReflectionClass($this);
        }
        
        return $this->reflection;
    }
}

Myślę, że kod jest czytelny i jasny. Dla przypomnienia odczytanie stałych zadeklarowanych w klasie jest realizowane z wykorzystaniem mechanizmu refleksji, który pozwala na analizowanie struktury/budowy obiektu w trakcie wykonania kodu.

Teraz bardzo prosty kod z przykładem użycia:

<?php
function processOption(Opcje $opcja)
{
    echo($opcja);
}

class Opcje extends MyEnum
{
    const __default = 2;
    
    const OPCJA1 = 1;
    const OPCJA2 = 2;
}

$obj = new Opcje();
processOption($obj);

Powyżej znajduje się definicja funkcji, która przyjmuje parametr typu Opcje i definicja tej klasy. Klasa ogranicza się jedynie do dziedziczenia po MyEnum i deklaracji stałych, cała funkcjonalność jest dziedziczona. Przykładowy kod wyświetli tylko 2, ponieważ taka jest wartość domyślna typu wyliczeniowego i typ zgadza się z oczekiwanym typem parametru funkcji.

W przypadku próby utworzenia obiektu z wartością spoza zdefiniowanych stałych zostanie rzucony wyjątek UnexpectedValueException, natomiast przy próbie przekazania do funkcji parametru innego typu:

<?php
processOption(2);
// wynik: Catchable fatal error: Argument 1 passed to processOption() 
// must be an instance of Opcje, integer given, called in...

Wygląda na to, że problem jest rozwiązany – nie można pomyłkowo przypisać innej wartości i tego nie zauważyć.

Dodatkowo, na polskiej planecie PHP pojawił się jakiś czas temu wpis z pomysłem implementacji typów wyliczeniowych w oparciu o koncepcję z Javy – odsyłam do tego wpisu (autorem jest Sebastian Malaca). Warto również przyjrzeć się komentarzom pod tym wpisem, ponieważ można tam znaleść kolejną/bardziej rozbudowaną alternatywę dla SplEnum.

PART – tester hostingu

Często jest tak, że aplikacja korzysta z jakichś bibliotek, modułów lub zakłada pewne opcje konfiguracyjne. Przykładem może być sprawdzanie MIME pliku z wykorzystaniem modułu Fileinfo. PHP w wersjach przeznaczonych na Windowsa wymaga ręcznej edycji php.ini i włączenia wczytywania tego modułu. Efekt jest taki, że w środowisku *nix wszystko działa, a po uruchomieniu tego samego kodu na Windowsie jest problem…

Mając chwilę wolnego czasu napisałem kawałek kodu, który pomaga znajdować tego typu problemy z zależnościami, konfiguracją przed uruchomieniem docelowej aplikacji – mowa o PHP Applications Requirements Tester (PART). Projekt znajduje się na moim koncie w serwisie GitHub.

Użycie jest według mnie bardzo proste – przykładowy plik z testami znajuje się również na GitHubie. Wystarczy utworzyć nowy plik PHP, dołączyć plik part.php, utworzyć obiekt PART i wywoływać na nim odpowiednie metody z testami środowiska. Taki zestaw umieszcza się w miejscu gdzie ma być aplikacja i uruchamiamy – w przeglądarce lub z shella. Mogą wystąpić różnice w konfiguracji między środowiskiem shell i webowym (ponieważ mają osobne pliki php.ini, więc najlepiej uruchamiać test z tego środowiska w którym będzie działała docelowa aplikacja.

Poniżej źródło przykładowego zbioru testów i wyniki:

<?php

require_once 'part.php';

$obiekt = new PART();
$obiekt->checkConfigHasValue('display_errors', 0)
    ->checkPDO()
    ->checkPDODriver('mysql')
    ->checkPHPVersion(5.4)
    ->checkDisableMagicQuotes()
    ->checkDefaultTimezone('Europe/Warsaw')
    ->checkNotWindowsServer();

Wynik konsolowy:

PHP Applications Requirements Tester - Report

Warning: The CLI environment may be different from web environment
Test name                                Result     Expected   Environment
================================================================================
Config [display_errors]                  OK         0                    
Extension loaded [PDO]                   OK                              
PDO driver [mysql]                       OK                              
PHP version                              OK         5.4 [>=]   5.4.9-4ubuntu2.3
Config [magic_quotes_gpc]                OK         0                    
Config [magic_quotes_runtime]            OK         0                    
Config [magic_quotes_sybase]             OK         0                    
Default timezone                         failure    Europe/Warsaw Europe/Berlin
Not Windows OS serwer                    OK                              

Warning: The CLI environment may be different from web environment

Wynik HTML:
PART wynik

Na załączonych raportach widać, różnicę pomiędzy uruchomieniem testu z konsoli i przeglądarki – w pierwszym wypadku display_errors jest wyłączone zgodnie z oczekiwaniami, w drugim przeciwnie.