Przez ostatnie trzy części “PHP w chmurze” poznawaliśmy Azure Storage, czy mechanizmy przechowywania danych w Windows Azure. Dla przypomnienia – były to bloby, kolejki oraz tabele. Ponieważ we wspomnianych wpisach skupiłem się na wyjaśnieniu jak one działają, a nie na ich praktycznych zastosowaniach, mechanizmy te mogły wydawać się nieco oderwane od rzeczywistości. Dzisiaj, zainspirowany jednym z tutoriali z serwisu Windows Azure for PHP, postanowiłem napisać jego nieco ulepszoną wersję.
Budowa aplikacji
Aplikacja nad którą będziemy pracować, nie będzie wiele robiła. Jej jedynym zadaniem będzie wysyłanie maili. W prawdziwym życiu, aplikacja taka stanowiłaby część większej całości. Aby nie zaciemniać obrazu, zastosowałem kilka skrótów, które w produkcyjnych aplikacjach nie powinny mieć miejsca.
Aplikacja będzie podzielona na dwie części – web role oraz worker role. Gwoli przypomnienia wyjaśnię, że rola typu web jest typową aplikacją internetową, jaką wszyscy znamy i lubimy – posiada graficzny interfejs użytkownika, można się do niej dostać z dowolnego miejsca Internetu, a odebrane dane zapisuje w celu dalszej ich obróbki. Z kolei rola typu worker, to tania siła robocza, która zajmuje się przetwarzaniem danych. Idealnie nadaje się do wykonywania zadań, mogących spowodować spadek wydajności roli web. W pewnym sensie workera można przyrównać do usługi, która działa w tle zawsze gotowa do pracy.
W naszej aplikacji rola web będzie formularzem odbierającym dane od użytkownika i zapisującym je w Azure Storage, a worker na ich podstawie będzie wysyłał emaile. Dlaczego wysyłką maili ma się zajmować worker, a nie web? Z bardzo prostego powodu. Jeśli serwer pocztowy nie będzie dostępny, wówczas rola web (widoczna dla użytkownika) przestanie działać lub w najlepszym wypadku będzie zgłaszała błąd w po wysłaniu formularza. Worker z kolei, mimo iż napotka ten sam problem, zgłosi go po cichu (z dala od wścibskich oczu). W ten sposób użytkownik cieszy cię, że strona działa szybko, dane nie są tracone, a my wysyłamy armię wyszkolonych małp do naprawienia problemu.
Do komunikacji między rolami, wykorzystane zostaną kolejki, załączniki będą przechowywane jako bloby, a tabele posłużą nam jako pojemnik na informacje wysłane przez użytkownika.
Na koniec warto wspomnieć, iż formularz, walidację danych oraz wysyłkę maili zleciłem Zend Frameworkowi. Szybko, elegancko, bez marnowania czasu.
Tworzenie projektu
Wiemy już jakie role będą nam potrzebne (jedna web i jedna worker) oraz z jakich mechanizmów przechowywania danych będziemy korzystać, czas rozpocząć prace nad aplikacją. Zaczniemy od utworzenia nowego projektu w Eclipse (instalację środowiska znajdziecie w pierwszym wpisie serii).
Z menu File wybieramy opcję New –> Project, a następnie Windows Azure PHP Project.
Jako nazwę projektu wpisujemy azuremail i klikamy przycisk Next. W kolejnym oknie w polu Role name wpisujemy web1, zaznaczamy checkbox Windows Azure Data Storage oraz ponownie klikamy przycisk Next
W ostatnim kroku dodajemy rolę worker.
Nazywamy ją worker1, skrypt startowy (Startup Script) nazywamy mailsender.php oraz zaznaczamy checkbox Windows Azure Data Storage.
Po kliknięciu w przycisk Finish, Eclipse stworzy trzy projekty. Pierwszy o nazwie takiej, jaką wpisaliśmy w pierwszym kroku tworzenia nowego projektu, zawiera jedynie pliki konfiguracyjne. Dwa pozostałe to nasze role, zdefiniowane w procesie tworzenia projektu. W przypadku projektów odpowiadających rolom możecie bez żadnych oporów usunąć pliki BlobSample.php, SampleEntity.php, TableSample.php oraz WindowsAzure.jpg.
Rola web
Rola web będzie prostym formularzem złożonym z kilku pól. Ponieważ będziemy korzystać z Zend Frameworka, musimy wprowadzić drobne modyfikacje do projektu. Najpierw musimy zmienić nazwę katalogu WindowsAzureSDKForPHP na library i dodać do niego klasy Zend Frameworka. Następnie w pliku php.ini, znajdującym się w katalogu php, zmieniamy wartość include_path na „.\library”.
Wreszcie możemy zabrać się za kodowanie. Na pierwszy ogień idzie formularz. Jest to standardowa klasa formularza dziedzicząca po Zend_Form. Klasę zapisujemy w pliku Form.php w katalogu głównym (tam gdzie znajduje się index.php). W formularzu celowo zrezygnowałem z filtrowania danych. W środowisku produkcyjnym filtrowanie danych jest niezbędne.
class Form extends Zend_Form
{
public function init()
{
$this->setName('form_email');
$this->setView(new Zend_View());
$subject = new Zend_Form_Element_Text('subject');
$subject->setLabel('Temat wiadomości')
->setRequired(true)
->addValidator(new Zend_Validate_NotEmpty())
->addValidator(new Zend_Validate_StringLength(array('max' => 250)));
$sender = new Zend_Form_Element_Text('sender');
$sender->setLabel('Nadawca')
->setRequired(true)
->addValidator(new Zend_Validate_NotEmpty())
->addValidator(new Zend_Validate_EmailAddress());
$message = new Zend_Form_Element_Textarea('message');
$message->setLabel('Treść wiadomości')
->setAttribs(array('rows' => 4, 'cols' => 30))
->setRequired(true)
->addValidator(new Zend_Validate_NotEmpty())
->addValidator(new Zend_Validate_StringLength(array('max' => 500)));
$attachment = new Zend_Form_Element_File('attachment');
$attachment->setValueDisabled(true)
->setLabel('Załącznik');
$send = new Zend_Form_Element_Submit('btn_send');
$send->setLabel('Wyślij')
->setIgnore(true);
$this->addElement($subject);
$this->addElement($sender);
$this->addElement($message);
$this->addElement($attachment);
$this->addElement($send);
}
}
Kolejną klasą jaką musimy dodać jest encja odzwierciedlająca wiadomość email. Nazwijmy ją MailEntity i również umieśćmy w katalogu głównym.
class MailEntity extends Microsoft_WindowsAzure_Storage_TableEntity
{
/**
* @azure subject
*/
public $subject;
/**
* @azure sender
*/
public $sender;
/**
* @azure message
*/
public $message;
/**
* @azure attachment
*/
public $attachment;
}
Na koniec pozostało dodanie obsługi formularza.
// do poprawnego wyświetlania polskich znaków
header('Content-Type: text/html; charset=UTF-8');
// zendowy autoloader - po co mamy się męczyć w ręczne dołączanie plików
require_once 'Zend/Loader/Autoloader.php';
$autoloader = Zend_Loader_Autoloader::getInstance();
// dodanie przestrzeni nazw klas Microsoftu
$autoloader->registerNamespace('Microsoft_');
// dolaczenie naszych klas
require_once 'Form.php';
require_once 'MailEntity.php';
// stworzenie formularza
$form = new Form();
// obsługa formularza
if(isset($_POST['btn_send'])) {
if($form->isValid($_POST)) {
// klucz partycji
$partitionKey = 'p1';
// klucz wiersza
$rowKey = md5(microtime() . uniqid());
// tworzymy obiekt encji i uzupełnianiamy go danymi
$mailEntity = new MailEntity($partitionKey, $rowKey);
$mailEntity->subject = $form->getValue('subject');
$mailEntity->sender = $form->getValue('sender');
$mailEntity->message = $form->getValue('message');
// wysłano plik
$fileInfo = $form->getElement('attachment')->getFileInfo();
if($fileInfo['attachment']['error'] == 0) {
$ext = pathinfo($fileInfo['attachment']['name'], PATHINFO_EXTENSION);
$fileName = md5($fileInfo['attachment']['name']) . '.' . $ext;
$mailEntity->attachment = $fileName;
// stworzenie bloba do przechowania przesłanego pliku
$storageBlob = new Microsoft_WindowsAzure_Storage_Blob();
$storageBlob->createContainerIfNotExists('attachments');
$storageBlob->putBlob(
'attachments',
$fileName,
$fileInfo['attachment']['tmp_name']
);
}
// zapisanie encji do tabeli
$storageTable = new Microsoft_WindowsAzure_Storage_Table();
$storageTable->createTableIfNotExists('emails');
$data = $storageTable->insertEntity('emails', $mailEntity);
// dodanie wiadomosci do kolejki i nowym mailu
$storageQueue = new Microsoft_WindowsAzure_Storage_Queue();
$storageQueue->createQueueIfNotExists('emails');
$storageQueue->putMessage('emails', $rowKey);
echo 'Wysłano maila';
}
}
// wyświetlenie formularza
echo $form;
I już. Wystarczyło kilkadziesiąt wierszy kodu, do stworzenia prostego interfejsu służącego do wysyłania maili. Możemy uruchomić projekt, wysłać kilka razy formularz i sprawdzić czy dane poprawnie zapisały się w Azure Storage.
Rola worker
Worker będzie jeszcze prostszy niż web.
// zendowy autoloader - po co mamy się męczyć w ręczne dołączanie plików
require_once 'Zend/Loader/Autoloader.php';
$autoloader = Zend_Loader_Autoloader::getInstance();
// dodanie przestrzeni nazw klas Microsoftu
$autoloader->registerNamespace('Microsoft_');
// dołączenie klasy Encji
require_once 'MailEntity.php';
// stworzenie obiektów magazynu danych
$storageQueue = new Microsoft_WindowsAzure_Storage_Queue();
$storageTable = new Microsoft_WindowsAzure_Storage_Table();
$storageBlob = new Microsoft_WindowsAzure_Storage_Blob();
// konfiguracja SMTP dla Gmaila
$config = array(
'auth' => 'login',
'username' => 'nazwa_uzytkownika',
'password' => 'haslo_uzytkownika',
'port' => 465,
'ssl' => 'SSL'
);
$transport = new Zend_Mail_Transport_Smtp('imap.gmail.com', $config);
// nieskonczona pętla
while(true) {
// pobranie 10 wiadomości z kolejki
$messages = $storageQueue->getMessages('emails', 10);
// jesli w kolejce znajduja sie wiadomości, obsłuż je
if(count($messages) > 0) {
// operacje na pobranych wiadomościach
foreach($messages as $message) {
// pobranie klucza wiersza z wiadomości
$rowKey = $message->messagetext; $rowKey = $message->messagetext;
// pobranie encji z tabeli
$mailEntity = $storageTable->retrieveEntityById('emails', 'p1', $rowKey, 'MailEntity');
// utworzenie maila i wypelnienie go danymi z encji
$mail = new Zend_Mail('utf-8');
$mail->addTo('adres_email_odbiorcy');
$mail->setSubject($mailEntity->subject);
$mail->setBodyText($mailEntity->message);
$mail->setFrom($mailEntity->sender);
// dołączenie załączników do maila
if(count($mailEntity->attachment) > 0) {
$mail->createAttachment($storageBlob->getBlobData('attachments', $mailEntity->attachment));
}
// wysłanie maila
$mail->send($transport);
unset($mail);
// usunięcie wiadomości z kolejki
$storageQueue->deleteMessage('emails', $message);
}
}
// uśpienie skryptu na 5 sekund
sleep(5);
}
Cała tajemnica, jaką skrywa worker, tkwi w nieskończonej pętli while. Podczas każdej iteracji skrypt sprawdza, czy w kolejce znajdują się wiadomości. Jeśli tak, to pobrane wiadomości przekazywane są do pętli foreach, w której dla każdej wiadomości pobierana jest encja zawierające niezbędne dane do wysłania maila. Jeśli do wiadomości został dodany załącznik, zostanie on również pobrany. Na koniec wszystko trafia do Zend_Mail, który wysyła wiadomość korzystając z Gmaila.
W powyższym kodzie zabrakło walidacji danych pobranych z tabeli oraz weryfikacji, czy mail rzeczywiście został wysłany. Podobnie jak poprzednio, nie dodałem tych elementów, aby nie bałaganić w kodzie. W środowisku produkcyjnym oba te elementy są niezbędne.
Na pewno zastanawiacie się dlaczego na końcu pętli while znajduje się funkcja sleep. Powody są dwa. Pierwszy – wydajność. Jeśli nie uśpimy pętli chociaż na jedną sekundę, mocno wzrośnie nam zużycie procesora. Drugim powodem jest koszt. Jeśli nie uśpimy skryptu, w czasie jednej sekundy może on wykonać sporą ilość operacji, za które płacimy. W Windows Azure płacimy dosłownie za wszystko, nawet za dostęp do magazynu. Dlatego pozostawienie skryptu bez jego uśpienia, uruchomionego przez dłuższy czas, może nas sporo kosztować.
Podsumowanie
Stworzyliśmy dzisiaj aplikację złożoną z dwóch ról oraz korzystającą ze wszystkich mechanizmów Azure Storage. Jak widać nie różni się to zbytnio od tworzenia standardowych aplikacji internetowych. Moglibyśmy bez większego problemu przekształcić rolę web w osobną aplikacją umieszoną na dowolnym hostingu i nadal mieć możliwość wysyłania danych do Windows Azure.
Na końcu piszesz o uśpieniu skryptu, ale czy umieszczenie linijki:
if(count($messages) == 0) continue;przed sleep() nie spowoduje, że gdy w kolejce nic nie będzie, to skrypt jednak nie będzie czekał?
@michal
Masz całkowitą rację. W momencie natrafienia na continue, pętla przechodzi do następnej iteracji i nie dochodzi do sleep.
Dzięki za zwrócenie uwagi na tego babola. Poprawiłem kod.
teraz to ten if w ogóle zbędny jest
jak $messages jest puste, to i tak pętla się nie wykona, no ale to już detal.
W takich sytuacjach zawszę korzystam z if-a. Dzięki temu wyraźnie widzę co się dzieje w kodzie. Kwestia przyzwyczajenia.
Tak, po przeczytaniu cz.6, z praktycznym przykładem, Twój opis AZURE jest kompletny.
Super artykuł o AZURE w PHP, do tej pory znałem tylko AZURE w VS 2010 – C#, super.