Nie ignorowanie błędów to podstawa

17 February 2012

Działanie wg zasady fail early, fail often and learn jest nie tylko dobrą zasadą dotyczącą "osobistego" rozwoju, ale pasuje idealnie do programowania. Paradoksalnie lepiej jest rozbić się z hukiem na jakimś błędzie i doświadczyć go jak najwcześniej - najlepiej już w trakcie programowania, niż skrzętnie omijać problem i udawać, że go nie ma. Jednym z najlepszych sposobów na zrobienie stabilnego/wiarygodnego programu jest pamiętanie o częstym rzucaniu wyjątków (oczywiście tam gdzie jest to potrzebne). Dzięki takiemu podejściu unikamy w późniejszym okresie życia danego programu, marnowania godzin debugowania i szukania przyczyny nieprawidłowego działania.

Tylko co z tym "okropnym" językiem PHP? Obecnie wszystko niby swoim miejscu, mamy przecież możliwość rzucania i łapania wyjątków. PHP ma długą historię - większość funkcji standardowych powstała przed dodaniem elementów obiektowości do PHP - wiele z tych funkcji zwraca wartość logiczną lub 0 w przypadku niepowodzenia. Z tego względu każda nowa aplikacja, jeśli jest napisana z wykorzystaniem obiektowości i korzysta z funkcji standardowych PHP, musi mieć na uwadze, że u jej podstaw leży kod mający w założeniu działać proceduralnie. Ciche "prześlizgnięcie" się po wyrażeniu będącym źródłem błędu to najgorsza z możliwych opcji. Nic nie zastąpi przygody, w której program wysypuje się w miejscu, w którym nie ma prawa się wysypać - z powodu tego, że o wiele wcześniej, w zupełnie innym miejscu coś poszło nie tak.

Częściowym ratunkiem jest restrykcyjne raportowanie błędów - w PHP można to skonfigurować w pliku php.ini jak również w trakcie wykonywania programu. Wyjątki dają możliwość na zalogowanie szczegółów błędu i zwrócenie użytkownikowi "normalnie" wyglądającej wiadomości. Tylko jak ta oczywistość ma się do szarej programistycznej rzeczywistości...

Poniżej jest metoda, którą próbowałem napisać zgodnie z powyższą ideą, na potrzeby aplikacji, która działała w oparciu o Symfony2. Ma ona na celu wyprodukowanie wersji obrazka przeskalowanej proporcjonalnie, przycięcie go ze wszystkich stron do podanych wymiarów i zapisanie w podanej lokalizacji (czyli rescale and crop). W internecie znalazłem wiele gotowych rozwiązań ale większość jest zbyt zagmatwana - przecież ja zawsze wiem lepiej!

    public static function createScaledImage($sourcePath, $targetPath, $targetWidth, $targetHeight) 
    {
        $sourceImage = imagecreatefromstring(file_get_contents($sourcePath));

        if (!$sourceImage) {
            throw new FileException('File passed as source could not be read or does not exist.');
        }

        $sourceWidth = imagesx($sourceImage);
        $sourceHeight = imagesy($sourceImage);

        if (!$sourceWidth || !$sourceHeight)
        {
            throw new FileException('Width or height of given image could not be read succesfully.');
        }

        $scaleX = $targetWidth / $sourceWidth;
        $scaleY = $targetHeight / $sourceHeight;

        $scale = max($scaleX, $scaleY);

        $scaledWidth = (int) ceil($sourceWidth * $scale);
        $scaledHeight = (int) ceil($sourceHeight * $scale);

        $offsetX = (int) floor(($targetWidth - $scaledWidth) * 0.5);
        $offsetY = (int) floor(($targetHeight - $scaledHeight) * 0.5);

        $resizedImage = imagecreatetruecolor($scaledWidth, $scaledHeight);        
        $result = imagecopyresampled($resizedImage, $sourceImage, 0, 0, 0, 0, $scaledWidth, $scaledHeight, $sourceWidth, $sourceHeight);

        if (!$result)
        {
            throw new FileException('There was an error during scaling image data to desired dimensions.');
        }

        $croppedImage = imagecreatetruecolor($targetWidth, $targetHeight);
        $result = imagecopyresampled($croppedImage, $resizedImage, $offsetX, $offsetY, 0, 0, $scaledWidth, $scaledHeight, $scaledWidth, $scaledHeight); 

        if (!$result)
        {
            throw new FileException('There was an error during cropping image data to desired dimensions.');
        }

        $result = imagejpeg($croppedImage, $targetPath, 95);

        if (!$result)
        {
            throw new FileException('There was an error during saving image data to destination path.');
        }

        imagedestroy($sourceImage);
        imagedestroy($resizedImage);
        imagedestroy($croppedImage);

        return true;
    }    

FileException to jedna z klas Symfony2, która wydawała mi się najbliższa znaczeniowo temu co ten kod wykonuje. W tym przypadku wystąpienia wyjątku może on zostać obsłużony w kontrolerze - należy zalogować błąd za pomocą usługi logger oraz można wyswietlić ją jako flash message (o ile nie zawiera ona żadnych poufnych danych).

Powyższa funkcja wykonuje zbyt dużo zadań - możnaby rozbić ją na kilka mniejszych.