PHP’de Composer ile Bağımlılık Yönetimi – 2

composer-2

Merhabalar,

Bu yazımla, yaklaşık 1 yıl önce birinci yazısını yazdığım “PHP’de Composer ile Bağımlılık Yönetimi” serisinin devamını getiriyor olacağım. Elbette sizlere ikinci yazının neden bu kadar geciktiğinin açıklamasını borçlu olduğumun farkındayım. Üniversite(!) bittikten sonra dikey geçiş sınavına hazırlanma sürecine başladım fakat benim düşünce yapıma uygun bi’şey olmadığı için biraz psikolojik sorunlar yaşadım (hâlâ daha ara ara yaşıyorum). Bir de bazı sağlık sorunlarım oldu buna ek olarak. Bu da haliyle yazı yazma hevesimi baltaladı, bu süre zarfında doğru dürüst, hiçbir şeye odaklanamadım. Her neyse bu konu hakkında ayrı bir yazı yazarım sanırım, şimdi bu yazının konusundan sapmayalım.

İlk yazımda hali hazırda var olan bir kütüphaneyi projemize dahil etmeyi ve kullanmayı öğrenmiştik. Bu yazımda kendi kütüphanemizi geliştirmeyi ve bunu autoloading sistemine eklemeyi öğreniyor olacağız. Yine başlıklar şekilde konuyu irdelemeye başlayalım.

Geliştirme Ortamımızı Hazırlamak

Öncelikle tabii ki geliştirme ortamımızı hazırlamakla başlamamız gerekiyor. Bunun için bir önceki yazımda anlattığım komut olan composer init komutunu çalıştırıyoruz. Ben bu şekilde bir composer.json dosyası oluşturdum:

{
"name": "erenhatirnaz/bagimlilik-yonetimi-2",
"type": "project",
"license": "MIT",
"authors": [
{
"name": "Eren Hatirnaz",
"email": "erenhatirnaz@hotmail.com.tr"
}
]
}
view raw composer.json hosted with ❤ by GitHub

Siz de kendinize özel bir composer.json dosyası oluşturabilirsiniz. Ardından src ve tests adında iki de klasör oluşturuyoruz. src klasörünü, geliştireceğimiz kütüphanenin kaynak kodlarının bulunduğu klasör olarak; tests klasörünü ise geliştirdiğimiz kütüphanenin birim testlerini (Unit testlerini) yazacağımız klasör olarak kullanacağız. Bir de, ben, git versiyon yönetim sistemini kullandığım için git init komutunu çalıştırıyorum fakat isterseniz siz çalıştırmayabilirsiniz. Bu yazımda, bir önceki yazımda kullanmış olduğum raporlama sistemine benzer, fakat daha basit bir raporlama sistemi hazırlıyor olacağız, örnek olarak.

Kütüphanemizi Kodlayalım

Geliştirme ortamımızı hazırladıktan sonra herhangi bir IDE ya da text editor ile proje klasörümüzü açabiliriz. Ben bu aralar Visual Studio Code kullanıyorum. Siz kendinizi hangi IDE ya da text editor’de rahat ve mutlu hissediyorsanız onu kullanabilirsiniz. Proje klasörümüzü açtıysak kodlamaya başlayabiliriz. Öncelikle src klasörümüzün içerisine Logger.php isminde bir dosya oluşturalım ve içerisine aşağıdaki kodları yazalım. Kod satırlarını tek tek yazımda açıklamak istemiyorum çünkü hem yazımın konusunun dışında hem de tek tek açıklamak yazımı çok uzatacağı için yapmak istemedim ama yine de elimden geldiğince dökümante etmeye çalıştım kodlarımı, zaten çok komplike işlemler yapmıyoruz bağımlılık yönetimine ihtiyaç duyan birinin bu derece PHP bilgisine sahip olduğunu varsayıyorum ve devam ediyorum.

<?php
namespace AtinaLogger;
use DateTime;
use RuntimeException;
/**
* Class Logger
*
* @package AtinaLogger
* @author Eren Hatırnaz <erenhatirnaz@hotmail.com.tr>
*/
class Logger
{
/**
* @var string Log dosyasının yolu
*/
private $logFilePath = '';
/**
* @var resource|null Log dosyasının işlemleri için kullanılacak nesne
*/
private $fileHandle = null;
/**
* @var string Log dosyasının son satırını tutar
*/
private $lastLine = '';
/**
* Logger constructor.
*
* @param string $logDirectory Log dosyasının tutulacağı dizin
*/
public function __construct($logDirectory)
{
$logDirectory = rtrim($logDirectory, '\\/');
if (!file_exists($logDirectory)) {
mkdir($logDirectory, 0777, true);
}
$this->logFilePath = $logDirectory . DIRECTORY_SEPARATOR . "log_" . date("d-m-Y") . ".txt";
if (file_exists($this->logFilePath) && !is_writable($this->logFilePath)) {
throw new RuntimeException("Log dosyası yazılabilir değil. İzinleri kontrol edin.");
}
$this->fileHandle = fopen($this->logFilePath, 'a');
if (!$this->fileHandle) {
throw new RuntimeException("Dosya açılamadı. İzinleri kontrol edin.");
}
}
/**
* Sınıfın LogFilePath değerini geri döndürür.
*
* @return string LogFilePath değeri.
*/
public function getLogFilePath()
{
return $this->logFilePath;
}
/**
* Sınıfın LastLine değerini geri döndürür.
*
* @return string LastLine değeri.
*/
public function getLastLine()
{
return $this->lastLine;
}
/**
* Log işlemini gerçekleştirir.
*
* @param string $level Log girdisinin seviyesi. Örn: error, warning, info
* @param string $message Log girdisinin mesajı.
*/
public function log($level, $message)
{
$message = $this->formatMessage($level, $message);
$this->write($message);
}
/**
* Gelen değerleri log dosyasında tutulacağı formata dönüştürür.
*
* @param string $level Log'un seviyesi. Örn: error, warning, info
* @param string $message Log mesajı.
*
* @return string Formatlanmış log girdisi.
*/
private function formatMessage($level, $message)
{
$level = strtoupper($level);
return "[{$this->getTimestamp()}] [{$level}] {$message}" . PHP_EOL;
}
/**
* Çalıştırıldığı andaki tarih-saat bilgisinin milisaniye verisi ile birlikte geri gönderir.
*
* @return string Tarih-Saat bilgisi.
*/
private function getTimestamp()
{
$originalTime = microtime(true);
$micro = sprintf("%06d", ($originalTime - floor($originalTime)) * 1000000);
$date = new DateTime(date('Y-m-d H:i:s.' . $micro, $originalTime));
return $date->format('Y-m-d G:i:s.u');
}
/**
* Log giridisini dosyaya yazar
*
* @param string $message Log girdisi.
*/
private function write($message)
{
if (!is_null($this->fileHandle)) {
if (fwrite($this->fileHandle, $message) === false) {
throw new RuntimeException('Log dosyası yazılabilir durumda değil. İzinleri kontrol edin.');
} else {
$this->lastLine = trim($message);
}
}
}
/**
* Tüm işlemler bittiğinde log dosyasını kapatır.
*/
public function __destruct()
{
if ($this->fileHandle) {
fclose($this->fileHandle);
}
}
}
view raw Logger.php hosted with ❤ by GitHub

Yalnız bu noktada dikkatinizi çekmem gereken bir yer var, çünkü yazımın ilerleyen bölümlerinde tekrar bu kısımdan faydalanıyor olacağız:

<?php

namespace AtinaLogger;

Burada yaptığım şey bir namespace ismi (Türkçeye “isim alanı” olarak çevrilmiş fakat ben yazımın devamın namespace olarak kullanmaya devam edeceğim) belirlemek, bir nevi projemize isim veriyoruz diyebiliriz aslında. “Ne gerek var buna?” diye sorduğunuzu biliyorum, hemen cevaplayayım öyleyse: Bizimle aynı class ismini taşıyan başkalarının da dosyaları olabilir. İşte bu durumda hem kullanıcı hem de PHP hangisini kullanacağını anlaması için bir namespace ismine ihtiyaç duyuyor. Hem bu sayede kütüphanemiz daha düzenli ve tertipli oluyor. Bu arada hazırladığımız sınıfa ben Logger ismini verdim, siz kendiniz istediğiniz gibi bir isim verebilirsiniz.

Hazırlık kısmında bahsettiğim gibi sadece örnek amaçlı bir kütüphane olduğu için bir sınıfımız var, onu da elimden geldiğince basit bir şekilde kodlamaya çalıştım. Siz de bu kodları ya da buna benzer kodlar yazabilirsiniz ama kopyala-yapıştır yapmaktan kaçınalım lütfen, ne yaptığımızı anlayarak ilerlemek çok önemli yoksa bazı şeyler havada kalıyor.

Autoloading Sistemi ve PSR-0, PSR-4 Standartları

Kütüphanemizi yazdığımıza göre artık bunu autoloading sistemine eklemenin vakti geldi. Yalnız bunun için PSR-0 ya da PSR-4 standartlarının ne olduklarını biliyor olmamız gerek. İkisi de autoloading ile ilgili standartlardır. Aslında, PSR-4 standardı PSR-0’ın yerine getirildi ve PSR-0, PHP-FIG (PHP Framework Interop Group) tarafından artık “kullanımdan kaldırıldı” olarak işaretlendi fakat yine de kullanmaya devam eden kütüphaneler olduğu için anlatma gereği duyuyorum.

İki standardın da amacı, namespace ismini düzenli bir yapı haline getirmek ve böylece autoloading işlemi sırasında, ilgili class’ın hangi dosya yolunda olduğunu, sadece namespace ismine bakarak anlaşılabilir hale getirmek. Kurallara gelecek olursak:

PSR-0 standardına göre namespace isimleri;

  • \<VendorName(Sağlayıcı ismi)>\(<Namespace>\)*<ClassName(Sınıf ismi)> yapısına uygun OLMALIDIR,
  • Her namespace’in bir üst namespace’i OLMALIDIR (Yani Vendor Name).
  • Her namespace’in birden fazla alt-namespace’i OLABİLİR,
  • Autoloading sırasında, namespace ismindeki her \ karakteri, PHP’deki DIRECTORY_SEPARATOR sabiti ile değiştirilir,
  • Autoloading sırasında, sınıf ismi içerisindeki her `_` karakteri, PHP’deki DIRECTORY_SEPARATOR sabiti ile değiştirilir,
  • Sağlayıcı ismi, namespace ve sınıf isimlerinde büyük-küçük harf kombinasyonları OLABİLİR,

kurallarına uygun olmalıdır. Bu kurallara göre örnekler verecek olursak:

  • \Doctrine\Common\IsolatedClassLoader => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
  • \Symfony\Core\Request => /path/to/project/lib/vendor/Symfony/Core/Request.php
  • \Zend\Acl => /path/to/project/lib/vendor/Zend/Acl.php
  • \Zend\Mail\Message => /path/to/project/lib/vendor/Zend/Mail/Message.php
  • \namespace\package\Class_Name => /path/to/project/lib/vendor/namespace/package/Class/Name.php

İlk örneği biraz daha açmak gerekirse:

Doctrine = Sağlayıcı ismi,
Common = Namespace İsmi,
IsolatedClassLoader = Sınıf ismi,
/path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php = Dosya Yolu


PSR-4 standardına göre ise namespace isimleri;

  • \<NamespaceName>(\<SubNamespaceNames(Alt isim alanları)>)*\<ClassName(Sınıf ismi)> yapısına uygun OLMALIDIR,
  • Her sınıf isminin bir üst-namespace’i OLMALIDIR, (PSR-0’da buna Vendor Name demiştik bura da aynı tanımlamayı kullanabiliriz)
  • Bir namespace’in bir ya da birden çok alt-namespace’i OLABİLİR,
  • ‘_’ karakterinin özel bir önemi yoktur. Yani ‘_’ karakteri, PSR-0’da olduğu gibi DIRECTORY_SEPERATOR ile değiştirilmiyor.
  • Sınıf isimleri büyük veya küçük harflerin herhangi bir kombinasyonundan oluşabilir.

kurallarına uygun olmalıdır. Bunlarla birlikte diğer bazı kuralları öğrenmek için buraya tıklayabilirsiniz. Yine birkaç örnek vermekte yarar var:

  • \Acme\Log\Writer\File_Writer => ./acme-log-writer/lib/File_Writer.php
  • \Aura\Web\Response\Status => /path/to/aura-web/src/Response/Status.php
  • \Symfony\Core\Request => ./vendor/Symfony/Core/Request.php
  • \Zend\Acl => /usr/includes/Zend/Acl.php

Yine ilk örneği biraz daha açalım:

Acme = Sağlayıcı ismi,
Log = Namespace ismi,
Writer = Alt-Namespace ismi,
File_Writer = Class ismi,
./acme-log-writer/lib/File_Writer.php = Dosya yolu

Gördüğünüz gibi sadece \Acme\Log\Writer\File_Writer ifadesine bakarak çok kolay bir şekilde class dosyasının bulunduğu yolu öğrenebiliyoruz. Bunu aynı şekilde PHP’de yapabilir. İşte tam bu noktada autoloading sistemi devreye giriyor.

NOT: İki standart arasındaki fark ise class’ların dosya yoluna bakarak çok rahat anlayabilirsiniz. PSR-0’daki dosya yolu daha karışık iken, PSR-4’de bu düzelitmiş ve daha düzenli bir yapıya kavuşmuş.

Autoloading

Uzun zamandır PHP yazıyorsanız mutlaka include ve require gibi tanımlamalarla uğraşmışsınızdır. Her dosyanın başında ihtiyacınız olan dosyaları kullanabilmek için bunları kullanmak gerekirdi buda bir nebze olsun kod kirliliğine yol açıyordu. Neyse ki PHP 5 sürümü ile __autoload() fonksiyonu, ardından da, PHP 5.1.2 ile spl_autoload() ve spl_autoload_register() fonksiyonları autoloading işlemlerini daha kolay ve kullanışlı hale getirdi. Composer aracımızın da işte bu fonksiyonları kullanıyor aslında. Yani autoloading yapmak için ille de Composer kullanmanıza gerek yok bu iki fonksiyondan birisi ile kendi autoloading sisteminizi geliştirebilirsiniz. Fakat bizim konumuz bu değil, biz Composer aracı içerisindeki autoloading mekanizmasını kullanacağız.

Kütüphanemizi Composer Aracının Autoloading Sistemine Eklemek

Bunu yapmak için öncelikle composer.json dosyamızı açıyoruz ve authors dizisinin bitişi olan ] karakterinden sonra bir , ve devamında şunları ekliyoruz:

"autoload": {
    "psr-4": {
         "AtinaLogger\\": "src/"
     }
}

Burada gördüğünüz gibi PSR-4 standardını kullandık ve namespace ismimiz olan AtinaLogger‘in ana dosyalarının src isimli klasörde olduğunu belirttik. Bu eklemenin ardından konsolda proje dizinimiz içerisindeyken şu komutu çalıştırmamız gerekiyor:

composer dump-autoload

Bu komutun işlevi, autoloading sistemine dahil edilmiş yeni bir kütüphane var ise, bu güncellemeye göre autoloader sınıfını tekrar oluşturmak ve yeni eklenen kütüphaneleri autoloading sistemine eklemek ve doğal olarak, eğer vendor klasörü yoksa, bu komut çalıştırıldığında otomatik olarak oluşturulur çünkü ilk yazıdan da hatırlayağınız üzere autoload.php dosyası bu klasör içerisinde barındırılıyor.

Yazdığımız Kütüphaneyi Test Etmek

Kütüphanemizi yazdık, autoloading sistemine de ekledik, şimdi ise sırada autoloading sisteminden faydalanarak birim testlerimizi(Unit test) yazmak var. Bu aşamada da PHP bilginizin belli bir düzeyde ve test kavramına hakim olduğunuzu varsayıyorum aksi taktirde yazdığım her kodu ya da test kavramını anlatmak istersem yazı çok uzar ve asıl konudan sapmış olurum. Test kavramı için çok kabaca bir tanımlama yapmak gerekirse: Yazdığımız uygulamanın (kütüphane, api, web sitesi vb.), bizim beklediğimiz sonuçları üretip üretmediğini kontrol etmektir. Biz de çok basit birkaç test yazarak kütüphanemizi test etmeye başlamadan önce bir require-dev bağımlılığı eklememiz gerekiyor (require ve require-dev arasındaki farkı hatırlamak için serinin ilk yazısına bakabilirsiniz).

Konsolda proje dizinimiz içerisindeyken çalıştırmamız gereken komut:

composer require --dev phpunit/phpunit

composer.json dosyamıza artık bu kısım da eklenmiş oldu:

"require-dev": {
    "phpunit/phpunit": "^5.6"
}

Artık testlerimizi tests klasörü içerisinde yazmaya başlayabiliriz.

Bizim kütüphanemizde bir sınıf olduğu için bir tane de test sınıfı yazacağız. tests klasörü içerisine LoggerTest.php isminde bir dosya oluşturdum ve içerisine de şu kodları yazdım, yine elimden geldiğince dökümante etmeye çalıştım sınıfı:

<?php
namespace AtinaLogger;
use PHPUnit_Framework_TestCase;
class LoggerTest extends PHPUnit_Framework_TestCase
{
/**
* @var Logger
*/
private $logger;
/**
* @var string
*/
private $logDirectory = __DIR__ . DIRECTORY_SEPARATOR . "logs";
/**
* Her testten önce Logger nesnemizi tekrar oluşturuyoruz.
*/
public function setUp()
{
$this->logger = new Logger($this->logDirectory);
}
/**
* Log dosyasının başarılı bir şekilde oluşturulup oluşturulmadığı test ediyoruz.
*/
public function testIsCreatedLogFile()
{
$this->assertFileExists($this->logger->getLogFilePath());
}
/**
* Hatanın başarılı bir şekilde raporlanıp raporlanmadığını test ediyoruz.
*/
public function testIsSuccessfullyErrorLoging()
{
$this->logger->log("error", "bu bir hatadir");
$this->assertEquals($this->logger->getLastLine(), $this->getLastLine($this->logger->getLogFilePath()));
}
/**
* Uyarının başarılı bir şekilde raporlanıp raporlanmadığını test ediyoruz.
*/
public function testIsSuccessfullyWarningLoging()
{
$this->logger->log("warning", "bu bir uyaridir");
$this->assertEquals($this->logger->getLastLine(), $this->getLastLine($this->logger->getLogFilePath()));
}
/**
* Parametre olarak genel dosyanın son satırını geri döndürür. Doğrulama yapmak için yazıldı.
*
* @param string $filename Dosya yolu
*/
private function getLastLine($filename)
{
$data = file($filename);
return trim($data[count($data) - 1]);
}
}
view raw LoggerTest.php hosted with ❤ by GitHub

Elbette birkaç tane daha test yazılabilirdi ama ben örnek teşkil etmesi açısından kodladığım için çok üzerine durmadım. Burada dikkatinizi çekmek istediğim iki satır var:

<?php

namespace AtinaLogger;

use PHPUnit_Framework_TestCase;

class LoggerTest extends PHPUnit_Framework_TestCase
{

Bu satırları yazmışız fakat PHPUnit_Framework_TestCase sınıfını içeren dosya için hiç include ya da require tanımlaması yapmadık veyahut bizim yazdığımız Logger sınıfı için aynı tanımlamaları yapmadık. “Nasıl oluyor da, bu kodlar çalışıyor?” diye sorduğunuzu biliyorum. Bunun cevabı tabii ki autoloading sistemi. Fakat bir önceki yazımdan hatırlayacağınız üzere bunun çalışabilmesi için yine vendor klasörü içerisindeki autoload.php dosyasını require ya da include ile eklememiz gerekiyor ama burada o da yok. Çünkü burada PHPUnit aracını kullanıyoruz, bu aracın kendi ayarlarını tutuğu phpunit.xml.dist isminde bir dosya olması gerekiyor ki testlerin hangi dizinde olduğunu vb. gibi ayarları yapabilelim. Şimdi bu dosyayı oluşturalım hemen projemizin ana dizinine:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true"
stopOnFailure="false"
bootstrap="./vendor/autoload.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true">
<testsuites>
<testsuite name="common">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
<filter>
<blacklist>
<directory>./vendor</directory>
</blacklist>
</filter>
</phpunit>

Sizin de fark ettiğiniz üzere, bootstrap="./vendor/autoload.php" tanımlaması ile autoload sınıfımız testlerimiz çalışmadan evvel onlara hangi kütüphanenin nerede olduğu konusunda yardımcı oluyor ve onları require gibi bir tanımlamala ile test dosyamıza dahil ediyor.

Testleri Çalıştırmak

Testimizi de yazdıysak o halde çalıştıralım bakalım, nasıl bir sonuç ile karşılaşacağız, yine projemizin ana dizinindeyken konsolda:

./vendor/bin/phpunit

komutunu çalıştırdığımız taktirde, phpunit.xml.dist dosyasındaki ayarlarımız ile testlerimiz çalışıyor ve benim elde ettiğim sonuç bu şekilde:

phpunit-output

Yani bütün testlerim başarılı bir şekilde tamamlandı.

Özet

PHP’de Composer ile Bağımlılık Yönetimi” yazı serisinde, bu yazımda, öncelikle namespace kavramını ve PSR-0 ve PSR-4 ile bu namespace tanımlamalarının nasıl düzenlendiğini, ardından ise kendi geliştirdiğimiz kütüphaneyi bu standartlardan faydalanarak, Composer aracının autoloading sistemine dahil etmeyi öğrenmiş olduk. Bu yazımda yaptığım örneğe github üzerinden ulaşmak için buraya tıklayabilirsiniz.

Yazı Serisi

  1. PHP’de Composer ile Bağımlılık Yönetimi(1): Üçüncü parti kütüphaneyi projeye dahil etme
  2. PHP’de Composer ile Bağımlılık Yönetimi(2): Kendi kütüphenimizi autoloading sistemine dahil etmek

Programlama ile ilgili bir sonraki yazımda görüşmek üzere, esenle kalın…

PHP’de Composer ile Bağımlılık Yönetimi – 2” üzerine 4 yorum

  1. Hocam öncelikle eline sağlık 2 yazıda oldukça bilgilendirici olmuş, emeğine sağlık
    Sorunum “./vendor/bin/phpunit” kodunu yazdığımda çalışmıyor, “./”karakterlerini yazmadan denedim Laracast’te öyle demiş,denedim gene olmuyor,neden kaynaklı olabilir?

    Beğen

    1. Merhaba,
      Öncelikle geri bildiriminiz için çok teşekkür ederim. Yazdığım yazıların insanlara faydası dokunduğunu bilmek güzel bi’şey. Sorununuza gelecek olursak, “./vendor/bin/phpunit” yazdığınızda karşılaştığınız hatayı içeren bir ekran görüntüsünü paylaşabilirseniz daha iyi yardımcı olabilirim. Şu an sorunun nereden kaynaklandığını tahmin edebilmek çok zor.

      Beğen

  2. Eren Hocam merhaba,
    bazı şeyler kafamda asılı kaldı mesela src ve test klasörleri vendor klasörü dışında kaldı
    yazdığımız kütüphaneyi packagist e nasıl ekleyebiliriz?

    Beğen

    1. Merhaba Seyid Bey, Neyi kast ettiğinizi tam olarak anlayamadım ama zaten src/ ve test/ klasörleri’nin vendor/ klasörü dışında olması gerekiyor. Aradığınız cevap bu değilse lütfen sorununuz hakkında daha detaylı bilgi verin. O şekilde yardımcı olmaya çalışayım. Packageist’e kütüphanenizi eklemek için önce packageist’e üye olup sonra da buradan projenizi submit etmeniz gerekiyor: https://packagist.org/packages/submit. İyi günler. ________________________________

      Beğen

Yorum yazmak için;