Upload plików w Django

Upload plików w Django

W Django nie zapomniano o kwestii uploadu plików. Deweloperzy tego frameworka starali się uczynić tę czynność jak najprostszą mimo to prawidłowa realizacja tego zadania wymaga odrobiny wysiłku. Na początek przytoczę znaleziony na stackoverflow.com minimalny przykład prezentujący krok po kroku jak w Django pobrać od użytkownika plik i zapisać go na serwerze. Będzie on stanowił doskonały punkt wyjścia dla bardziej zaawansowanych tematów jak dbanie o równomierny rozkład plików w katalogach czy przycinanie obrazków w panelu administracyjnym z użyciem jQuery i jCrop, którymi to tematami zajmę się w kolejnych wpisach.

Poniższy opis oparty jest na przykładzie, którego źródło można ściągnąć z github-a

Przykład bazuje na Django 1.3 w którym główny plik settings.py i urls.py trzymane są w katalogu głównym projektu.

minimal-django-file-upload-example/
    src/
        myproject/
            database/
                sqlite.db
            media/
            myapp/
                templates/
                    myapp/
                        list.html
                forms.py
                models.py
                urls.py
                views.py
            __init__.py
            manage.py
            settings.py
            urls.py

W Django 1.4 nastąpiła zmiana w domyślnej strukturze projektu i wyżej wspomniane pliki powinny trafić do podkatalogu o nazwie odpowiadającej nazwie projektu.

1. Konfiguracja: /myproject/settings.py

Przede wszystkim należy zacząć od prawidłowej konfiguracji bazy danych ponieważ szczegółowa ścieżka do pliku zapisywana będzie w bazie danych, mediów – czyli wskazania katalogu w projekcie, do którego będą trafiały wszystkie wgrywane przez użytkowników pliki oraz URL-a zarezerwowanego na potrzeby serwowania plików statycznych. Trzeba też pamiętać aby aplikacja zawierająca model ze zdefiniowanymi polami do uploadu plików była dodana do INSTALLED_APPS.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': '/path/to/myproject/database/sqlite.db'),
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    }
}

MEDIA_ROOT = '/path/to/myproject/media/'
MEDIA_URL = '/media/'

INSTALLED_APPS = (
 …
 'myapp',
)

Więcej na temat konfiguracji mediów i plików statycznych w Django.

2. Model: myproject/myapp/models.py

Do uploadu plików w modelu Django służy pole FileField lub też jego specjalna wersja dla obrazków ImageField. Oba z wymienionych pól mają atrybut „upload_to” umożliwiający wskazanie szczegółowej lokalizacji składowania pliku.

# -*- coding: utf-8 -*-
from django.db import models
 
class Document(models.Model):
    docfile = models.FileField(upload_to='documents/%Y/%m/%d')

Jeśli ustawimy upload_to=’documents/%Y/%m/%d’ to w katalogu określonym w zmiennej MEDIA_ROOT zostanie utworzona ścieżka oparta na bieżącej dacie np. „documents/2013/03/09”.

Atrybutowi „upload_to” możemy przypisać zarówno tekst, jak i też metodę czy funkcję zwracającą ścieżkę do katalogu, co daje nam większe możliwości, ale jest to kwestia bardziej złożona i omówię ją przy innej okazji.

3. Formularz: myproject/myapp/forms.py

Wykorzystanie obiektów formularzy w Django bardzo ułatwia pracę z formularzami o czym przekonałem się wielokrotnie. Także w tym przypadku najprościej jest użyć ModelForm – czyli formularza opartego na modelu, w którym nie trzeba definiować pól formularza tylko wskazać na jakim modelu dany formularz ma się opierać i które pola mają podlegać edycji. Przytaczany tu przykład jednak pokazuje jak stworzyć zupełnie niestandardowy formularz z wykorzystaniem kontrolki pola do uploadu pliku FileField. UWAGA! – nie należy mylić pól modelu z polami formularza, które mimo podobieństwa nazwy są czymś innym.

# -*- coding: utf-8 -*-
from django import forms
 
class DocumentForm(forms.Form):
    docfile = forms.FileField(
        label='Select a file',
        help_text='max. 42 megabytes'
    )

4. Widok: myproject/myapp/views.py

W widoku realizowany jest cały proces obsługi formularza. Po przeprowadzeniu walidacji możemy śmiało pobrać plik z request.FILES i przypisać go wprost do pola modelu. Jeszcze łatwiej byłoby w przypadku formularza opartego o model, ale tak za to mamy większą elastyczność gdyż przed zapisaniem pliku w bazie danych moglibyśmy jeszcze coś z nim zrobić (np. zmienić nazwę).

# -*- coding: utf-8 -*-
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
 
from myproject.myapp.models import Document
from myproject.myapp.forms import DocumentForm
 
def list(request):
    # Handle file upload
    if request.method == 'POST':
        form = DocumentForm(request.POST, request.FILES)
        if form.is_valid():
            newdoc = Document(docfile = request.FILES['docfile'])
            newdoc.save()
 
            # Redirect to the document list after POST
            return HttpResponseRedirect(reverse('myapp.views.list'))
    else:
        form = DocumentForm() # A empty, unbound form
 
    # Load documents for the list page
    documents = Document.objects.all()
 
    # Render list page with the documents and the form
    return render_to_response(
        'myapp/list.html',
        {'documents': documents, 'form': form},
        context_instance=RequestContext(request)
    )

5. Konfiguracja URL-i: myproject/urls.py

Django nie udostępnia mediów z MEDIA_ROOT automatycznie gdyż byłoby to niebezpieczne w środowisku produkcyjnym. Na serwerze developerskim to Apache, Nginx czy Lighttpd zajmują się serwowaniem plików statycznych natomiast w środowisku developerskim trzeba sobie dodać regułkę do urls-ów.

# -*- coding: utf-8 -*-
from django.conf.urls.defaults import patterns, include, url
from django.conf import settings
from django.conf.urls.static import static
 
urlpatterns = patterns('',
    (r'^', include('myapp.urls')),
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

,

6. URL-e aplikacji: myproject/myapp/urls.py

Aby widok z danej aplikacji był dostępny także dla niego trzeba dodać regułkę do urls.py

# -*- coding: utf-8 -*-
from django.conf.urls.defaults import patterns, url
 
urlpatterns = patterns('myapp.views',
    url(r'^list/$', 'list', name='list'),
)
<pre>
 
<h2>7. Szablon: myproject/myapp/templates/myapp/list.html</h2>
 
<p>Aby można było wgrać plik na serwer wymagane jest aby tag formularza zawierał atrybut enctype="multipart/form-data" oraz żeby formularz był wysyłany metodą POST</p>
 
<pre lang="html4strict">
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Minimal Django File Upload Example</title>
    </head>
    <body>
    <!-- List of uploaded documents -->
    {% if documents %}
        <ul>
        {% for document in documents %}
            <li><a href="{{ document.docfile.url }}">{{ document.docfile.name }}</a></li>
        {% endfor %}
        </ul>
    {% else %}
        <p>No documents.</p>
    {% endif %}
 
        <!-- Upload form. Note enctype attribute! -->
        <form action="{% url list %}" method="post" enctype="multipart/form-data">
            {% csrf_token %}
            <p>{{ form.non_field_errors }}</p>
            <p>{{ form.docfile.label_tag }} {{ form.docfile.help_text }}</p>
            <p>
                {{ form.docfile.errors }}
                {{ form.docfile }}
            </p>
            <p><input type="submit" value="Upload" /></p>
        </form>
    </body>
</html>

8. Uruchomienie przykładu

Tak dla pełnego obrazu – aby uruchomić omawiany tu minimalny, przykład uploadu plików w django należy ściągnąć jego źródła lub przekopiować z wyżej zamieszczonych listingów a następnie uruchomić w konsoli odpowiednie komendy:

> cd myproject
> python manage.py syncdb
> python manage.py runserver

Po uruchomieniu serwera pod adresem localhost:8000/list/ zobaczymy – na początek pustą – listę zuploadowanych plików oraz formularz. Pliki powinny się zapisywać w katalogu analogicznym do „/path/to/myproject/media/documents/2013/03/09/”

Autorem powyższego wyczerpującego przykładu jest Akseli Palén. Omówienie jest wzbogacone kilkoma moimi komentarzami.

Pliki statyczne w Django

Pracuję z Django od wersji 0.96 i w projektach, które utrzymuję jest jeszcze wiele zaszłości. Jedną z nich jest sposób składowania wszelakich plików statycznych w katalogu, którego ścieżka zdefiniowana była w settings.py w zmiennej MEDIA_ROOT. Tymczasem od wersji 1.3 developerzy Django postanowili rozróżnić pliki statyczne – czyli wszelkiego rodzaju javascript-y, css-y, grafiki itp. stałe elementy składające się na tzw. layout – od mediów, pod którą to nazwą powinniśmy rozumieć wszelkiego rodzaju pliki uploadowane.

Na marginesie dodam, że aplikacja django-staticfiles była już wcześniej dostępna, ale integralną częścią frameworka stała się dopiero od wersji 1.3.

Jak skonfigurować ustawienia mediów?

Zacznę od mediów bo sprawa jest stara, prosta i wielokrotnie opisywana.

Załóżmy, że struktura naszej aplikacji wygląda tak:

project_root/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py 
    news/
        __init__.py
        models.py
        urls.py
        views.py
    /media
        /users
	    user_avatar.jpg

UWAGA! Od wersji 1.4 zmieniła się podstawowa struktura projektu w ten sposób, że podstawowa konfiguracja oraz główny plik z urls.py trafiły do podkatalogu z nazwą projektu – w tym wypadku „mysite”.

Mediów dotyczą dwie zmienne w settings.py

MEDIA_ROOT – przechowuje pełną ścieżkę do katalogu z mediami. Do tego folderu będą trafiały pliki wrzucane przez użytkowników z poziomu strony www. Stanowi on bazową ścieżkę dla parametru „upload_to” pól FileField i ImageField.

MEDIA_ROOT = '/home/user/project_root/media'

Jeśli zmienimy fizyczne położenie mediów (bo np. serwer produkcyjny czyta pliki statyczne tylko z podkatalogu public_html) to będziemy musieli zmienić MEDIA_ROOT.

MEDIA_ROOT = '/home/user/project_root/public_html/media'

ale MEDIA_URL i wszystkie odwołania w szablonach pozostaną takie same.

MEDIA_URL – bazowy adres URL, pod którym dostępne będą pliki mediów.

MEDIA_URL = '/media/'

Przy takiej kongfiguracji do plików mediów będziemy odwoływać się następująco

<img src="/media/images/user_avatar.jpg" alt="{{ user.username }}" />

MEDIA_ROOT – Może mieć formę adresu względnego – najczęściej używaną na serwerze developerskim, albo adresu bezwzględnego, przydatnej na serwerach produkcyjnych

MEDIA_URL='http://static.domain.pl/media/'

Zmiana url-a wiąże się z koniecznością zmiany wszystkich odwołań w szablonach dlatego warto od razu przewidzieć taką możliwość i przekazać do szablonu zmienną MEDIA_ROOT i posługiwać się nią zamiast hardkodować adresy do plików mediów.

<img src="{{ MEDIA_URL }}images/user_avatar.jpg" alt="{{ user.username }}" />

Przy okazji wspomnę, że obiekty formularzy FileField i ImageField rzutowane do tekstu lub unicode-a zwracają względną ścieżkę do pliku

{{ user.avatar }} # users/user_avatar.jpg

metoda url zwraca pełen url zawierający w sobie MEDIA_URL

{{ user.avatar.url }} # /media/users/user_avatar.jpg (albo http://static.domain.pl/media/users/user_avatar.jpg)
<img src="{{ user.avatar.url }}" alt="{{ user.username }}" />

UWAGA! Jeśli dla FileField lub ImageField dopuścimy możliwość wstawiania null-i (null=True, blank=True, default=None), w szablonie zawsze dobrze jest sprawdzać czy mamy do czynienia z plikiem czy null-em.

<img src="{% if user.avatar %}{{ user.avatar.url }}{% endif %}" alt="{{ user.username }}" />

a metoda path zwraca pełną ścieżkę do pliku zawierającą w sobie MEDIA_ROOT

{{ user.avatar.path }} # /home/user/project_root/public_html/media/users/user_avatar.jpg

Z plikami statycznymi wiąże się więcej zagadnień.

Jaki jest modelowy sposób składowania plików statycznych w Django?

Przede wszystkim zmienna STATIC_ROOT pełni rolę analogiczną do MEDIA_ROOT, ale tylko na serwerze produkcyjnym. Wbudowany – developerski – serwer Django w celu odnalezienia położenia plików statycznych korzysta z tzw. finderów zdefiniowanych w STATICFILES_FINDERS oraz STATICFILES_DIRS – ale po kolei.

Pliki statyczne należy trzymać w podkatalogu „./static” każdej aplikacji

project_root/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py
        static/ - pliki wspólne dla całej strony
            /css
                screen.css
            /js
                jquery.js 
    musicplayer/
        __init__.py
        models.py
        urls.py
        views.py
        static/ - pliki statyczne aplikacji
            /js
                jplayer.js
            /images
                play.gif

Ścieżkę każdego z podkatalogów powinniśmy dodać do krotki zmiennej STATICFILES_DIRS w settings.py

STATICFILES_DIRS = (
    "/home/project_root/mysite/static",
    "/home/project_root/musicplayer/static",
)

Jeśli zdefiniujemy zmienną STATIC_URL następująco:

STATIC_URL = '/static/'

Adresy plików w szablonach będą wyglądały tak:

<img src="/static/images/play.gif" alt="play" />

Oczywiście tu podobnie jak w mediach warto przekazać do szablonu zmienną STATIC_URL,

<img src="{{ STATIC_URL }}images/play.gif" alt="play" />

bo na serwerze produkcyjnym możemy się zdecydować na podanie bezwzględnego url-a zamiast względnej ścieżki.

STATIC_URL = 'http://static.domain.pl/static/'

Czasami chcielibyśmy aby media aplikacji – dajmy na to musicplayer – znalazły się w podkatalogu o tej samej nazwie. Nie musimy w tym celu zmieniać struktury plików lecz wystarczy dodać prefix do ścieżki w STATICFILES_DIRS.

STATICFILES_DIRS = (
    "/home/project_root/mysite/static",
    ('musicplayer', "/home/project_root/musicplayer/static"),
)

Fizyczne położenie pliku się nie zmieniło, ale zmiana konfiguracji pozwala traktować plik jakby się znajdował w dodatkowym podkatalogu.

<img src="/static/musicplayer/images/play.gif" alt="play" />

Aby całość zadziałała musimy jeszcze wskazać Django narzędzia wyszukiwania plików tzw. findery.

STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)

Ponieważ w dwóch różnych podkatalogach /static możemy mieć plik o tej samej nazwie, może się zdarzyć że jeden „nadpisze” drugi i trzeba będzie wyszukać wszystkie lokalizacje w jakich może znajdować się plik o danej nazwie.

Warto wtedy skorzystać z komendy findstatic

$ python manage.py findstatic css/base.css admin/js/core.js
#/home/special.polls.com/core/static/css/base.css
#/home/polls.com/core/static/css/base.css
#/home/polls.com/src/django/contrib/admin/media/js/core.js

Pliki statyczne na serwerze developerskim

Django nie udostępnia plików statycznych i mediów z automatu gdyż byłoby to niebezpieczne na serwerze produkcyjnym. Dlatego też także w środowisku developerskim trzeba zadbać aby serwer wiedział gdzie szukać plików poprzez dodanie odpowiednich reguł do urls.py

from django.conf.urls import patterns, include, url
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
urlpatterns = patterns('',
    # Uncomment the next line to enable the admin:
    url(r'^admin/', include(admin.site.urls)),
    ...
)
 
if settings.DEBUG:
    urlpatterns += staticfiles_urlpatterns()   
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Pliki statyczne na serwerze produkcyjnym

Nie będę opisywał jak skonfigurować Apache 2, Nginx czy Lighttpd. Przyjmijmy, że dowolny serwer umożliwia serwowanie plików statycznych z poziomu podkatalogu /public_html.

W pierwszej kolejności należy odpowiednio ustawić zmienną STATIC_ROOT w settings.py

STATIC_ROOT = os.path.join(PROJECT_ROOT, 'public_html/static')

Następnie należało by odpowiednio do ustawień STATICFILES_DIRS przekopiować (lub dowiązać) pliki i katalogi.

project_root/
    public_html/
        static/ - pliki wspólne dla całej strony
            /css
                screen.css
            /js
                jquery.js 
	    musicplayer/ - pliki statyczne aplikacji
                /js
                    jplayer.js
                /images
                    play.gif

Może to być nieco pracochłonne dlatego też wielu developerów do tej pory preferowało trzymanie statyków w jednym wspólnym katalogu. Po opublikowaniu strony na serwerze produkcyjnym wystarczyło tylko przekopiować lub przenieść statiki do katalogu publicznego oraz zmienić ustawienia STATIC_ROOT.

Od wersji 1.4 mamy do dyspozycji komendę „collectstatic”, który całą robotę z kopiowaniem plików statycznych robi za nas. W trakcie developmentu możemy trzymać pliki statyczne w wielu podkatalogach a po wrzuceniu na live wystarczy tylko wywołać komendę –

python manage.py collectstatic

i mamy przekopiowane wszystkie pliki statyczne łącznie z tymi związanymi z panelem administracyjnym. Dodanie na końcu opcji -l albo –link,

python manage.py collectstatic -l

tworzy dowiązania symboliczne zamiast kopiować pliki.

Staticfiles w starych projektach

Przy podnoszeniu starych projektów do nowej wersji Django nie jesteśmy zmuszani do wydzielania mediów i relokacji plików statycznych. Dalej możemy trzymać wszystko w jednym katalogu, ale trzeba zadbać o odpowiednią konfigurację.

MEDIA_ROOT = '/home/user/project_root/static'
 
MEDIA_URL = '/static/'
 
STATICFILES_DIRS = (
    MEDIA_ROOT,
)
 
STATIC_URL = MEDIA_URL
 
STATIC_ROOT = '/home/user/project_root/public_html/static'

UWAGA! Traktuj STATIC_ROOT jak zmienną używaną tylko na serwerze produkcyjnym i nawet jeśli nie wiesz jak powinna ta ścieżka wyglądać docelowo wskarz na katalog, który nie jest jednym z katalogów zdefiniowanych w STATICFILES_DIRS

Podusmowanie

Konfiguracja mediów w Django nie jest tak prosta jak to się wydaje doświadczonym developerom. Zagadnienie to nie jest poruszone w podstawowym tutorialu, a dokumentacja choć jedna z najlepszych nie prowadzi za rękę początkującego programistę.

Choć staticfiles wraz z komendą collectstatic to bardzo wygodne rozwiązanie ułatwiające trzymanie porządku w projekcie, dodatkowo utrudnia zagadnienie zarządzania plikami statycznymi. Mam nadzieję, że swoim wpisem nie gmatwam dodatkowo całej sprawy, i że przyda się on komuś rozpoczynającemu dopiero pracę z frameworkiem Django.

MySQL-owe widoki w Django

Niniejszy artykuł traktuje o widokach w bazie danych (konkretnie w MySQL) i możliwości tworzenia modeli do tychże widoków z użyciem ORM-a framewokra Django. Zwracam na to uwagę aby nie pomylić – pomimo zbieżności nazw – widoków SQL-owych z widokami (views.py) Django.

Czym są właściwie widoki w relacyjnej bazie danych i po co się je tworzy wyjaśnia – skądinąd świetnie napisany rozdział pod tytułem „Widoki” w przygotowanym przez Heliona kursie MySQL.

W jednym z projektów, nad którymi pracuję stanąłem przed potrzebą stworzenia widoku w MySQL-u. Postanowiłem więc zdefiniować taki widok, stworzyć do niego model w Django i posługiwać się jak dowolnym innym modelem z tym, że tylko do odczytu. ORM w Django jest jednym z najbardziej rozbudowanych i zaawansowanych narzędzi tego typu mimo to ma swoje ograniczenia dlatego też nie byłem pewien czy uda mi się zrealizować to co sobie założyłem. Okazało się to możliwe aczkolwiek dopiero od wersji 1.3 frameworka Django.

Poniżej prezentuję jak zdefiniować prawidłowo model dla widoku. Na potrzeby tego artykułu stworzyłem możliwie proste przykłady aby pokazać mechanizm. Skonstruowany przeze mnie widok był daleko bardziej rozbudowany.

Załużmy, że mamy nowy projekt django a w nim aplikację do prezentacji newsów.

news/models.py
from django.db import models
from django.contrib.auth.models import User
 
class Article(models.Model):
    name = models.CharField(max_length=255)
    slug = models.SlugField(unique=True, max_length=100)
    text = models.TextField()
    added_by = models.ForeignKey(User)
    added_on = models.DateTimeField(auto_now_add=True)
    updated_on = models.DateTimeField(auto_now=True)
    is_active = models.BooleanField(default=True)
news/admin.py
from news.models Article
from django.contrib import admin
 
class ArticleAdmin(admin.ModelAdmin):
    list_filter = ['is_active']
    list_display = ('name', 'added_by', 'added_on', 'updated_on', 'is_active')
    prepopulated_fields = {'slug': ('name',)}
    search_fields = ('name', )
 
admin.site.register(Article, ArticleAdmin)

Z pewnych powodów potrzebujemy widoku, który prezentuje się następująco:

DROP VIEW IF EXISTS `users_newsview`;
CREATE VIEW `users_newsview` AS
    SELECT 0 AS id, n.id AS news_id, n.name AS news_name, n.added_by_id AS user_id, u.username
    FROM news_article AS n
    INNER JOIN auth_user AS u ON u.id = n.added_by_id;

Tworzymy do niego model, który będzie tylko do odczytu.

users/models.py
from django.db import models
from news.models import Article
from django.contrib.auth.models import User
 
class NewsView(models.Model):
 
    class Meta:
        managed = False
 
    news = models.ForeignKey(Article, null=True, blank=True, on_delete=models.DO_NOTHING)
    news_name = models.CharField(max_length=255, null=True, blank=True)
    user = models.ForeignKey(User, null=True, blank=True, on_delete=models.DO_NOTHING)
    username = models.CharField(max_length=255, null=True, blank=True)

O tym, że obiekty modelu „NewsViews” są niemodyfikowalne decyduje Zdefniowany w podklasie „Meta” parametr „managed = False”. Innym bardzo istotnym elementem jest dodanie do definicji kluczy obcych „on_delete=models.DO_NOTHING”. Django w przypadku kluczy obcych domyślnie emuluje zachowanie „on delete cascade”. Możliwość zmiany tego zachowania Django wspiera dopiero od wersji 1.3. Jeśli nie zmienimy sposobu w jaki Django ma postępować z elementami powiązanymi w trakcie usuwania danego obiektu to przy próby usunięcia newsa, albo usera zostanie zgłoszony błąd gdyż próba usunięcia także rekordu widoku z oczywistych względów się niepowiedzie.

W starszej wersji frameworka Django też można stworzyć model do widoku, ale trzeba w nim zrezygnować z tworzenia relacji do innych obiektów i zamiast pól „news” i „user” zdefiniować pola typu integer „news_id” oraz „user_id”. Jak się domyślacie rozwiązanie to jest o wiele mniej wygodne.

Kilka reflaksji na temat frameworków

Ostatnio – po długiej przerwie – wszedłem na blog Zyxa wierząc, że znajdę tam coś ciekawego do przeczytania. Nie zawiodłem się. Znalazłem dwie recenzje najpopularniejszych w Polsce frameworków PHP tj. Symfony 1.4 okiem Zyxa i Zend Framework także okiem Zyxa.

Ostatnio przerzuciłem się na Pythona i Django, a wcześniej przez co najmniej dwa lata budowałem aplikacje w oparciu o Kohanę, jednak od czasu do czasu – w tak zwanym międzyczasie – próbowałem też coś sklecić na ZF i polubić Symfony dlatego też orientuję się przynajmniej pobieżnie w ich konstrukcji. Artykuły Zyxa i własne doświadczenie skłoniły mnie do kilku refleksji na temat ogólnej konstrukcji frameworków, sensu użycia ORM-ów a także systemów szablonów.

Architektura

Dobrze zaprojektowana struktura plików to podstawa porządnego frameworka. Każdy szanujący się projektant stara się utrzymać porządek w swoim projekcie i lubi bez zbędnego zastanawiania się wiedzieć gdzie ma czego szukać. Kohana umożliwia tworzenie modułów z których każdy może mieć swoje kontrolery, modele, widoki, pliki konfiguracyjne, a nawet biblioteki. Układ katalogów jest rozbudowany. Kaskadowość z jednej strony umożliwia elastyczność np. nadpisanie konfiguracji domyślnej, konfiguracją specyficzną dla danego modułu. (w Kohanej nie tylko konfigurację można nadpisać ale też „wymienić” klasy z zachowaniem ich dotychczasowej nazwy. Założenie to w PHP wyklucza proste użycie dziedziczenia dlatego też w Kohanie 2 było to zrobione po chamsku z użyciem evala. W wersji 3 tego frameworka mechanizm ten został bardziej elegancko zaimplementowany). Z drugiej strony przy bardziej rozbudowanych projektach człowiek zaczyna się gubić nie pamiętając niekiedy skąd się wzięła bieżąca wartość danej zmiennej konfiguracyjnej. Zaczyna się szukanie. Pół biedy, kiedy struktura katalogów jest płaska, ale jeśli musimy przeklikać się przez kilka poziomów zagłębień staje się to męczące.

Nie wiem kto na to wpadł, aby każdą klasę trzymać w osobnym pliku, nie wiem też kto wymyślił aby nazwa klasy odzwierciedlała położenie pliku w strukturze katalogów (patrz ZF, PEAR), ale doprowadziło to do powstania całej masy katalogów i podkatalogów i jeszcze większej liczby plików, z których niektóre np. zawierają jedynie jednolinijkową definicję wyjątku. W Django w poszczególnych app-sach znajdziemy z reguły pliki (__init__.py, views.py, models.py, urls.py, admin.py, tests.py) i to w 90% przypadków wystarcza. W Kohanie jeden moduł to kilka katalogów. Python jakoś obywa się bez autoloadera i kiedy czytam to co wyżej sam napisałem zaczynam rozumieć dlaczego. Na marginesie tylko wspomnę, że w związku z wprowadzeniem namespace-ów, sposób organizacji klas w plikach PHP się zmieni.

PHP w wersji piątej poszło wyraźnie w kierunku „magi”. Wszystkie poprzedzone podwójnym podkreśleniem metody są bardzo wygodnym rozwiązaniem i osobiście bardzo je lubię ale nadmiar czarów daje się we znaki w chwili kiedy zachodzi potrzeba prześledzenia procesów zachodzących w aplikacji. Debugowanie przesyconych „magią” klas jest znacznie utrudnione przede wszystkim przez niejednoznaczne komunikaty błędów. Dodatkową wadą użycia metod magicznych jest to, że dynamiczne settery i gettery nie będą podpowiadane przez żadne IDE typu NetBeans, czy Eclipse mimo w sumie dobrze zrealizowanej w nich funkcji podpowiadania składni.

Inną kwestią jest uniwersalność kodu. Klas Kohanej można używać tylko w ramach tego frameworka, z kolei biblioteki eZ Components lub Zend Frameworka można używać niezależnie lub w ramach zupełnie innej platformy. Sam wielokrotnie w projektach opartych na Kohanej sięgałem do wybranych komponentów Zend Frameworka. Klasy ZF są jak to zauważył Zyx dopracowane i przetestowane i jedyną ich wadą jest z reguły to, że są zbyt dobre. Zamiast w najprostszy sposób realizować banalną funkcjonalność, autorzy poszczególnych bibliotek prześcigają się w wymyślaniu wariantów zastosowań i sposobów użycia. Tak właśnie z noża powstał szwajcarski scyzoryk – fajny ale do smarowania chleba najlepszy jest zwykły nóż kuchenny. Ponoć w nowej wersji ZF ma to ulec zmianie.

ORM

Symfony zniechęciło mnie do siebie przede wszystkim ORM-em. Do Propela nawet nie startowałem, natomiast z Doctrine-m walczyłem jakiś czas. Niestety na etapie kiedy przeprowadzałem moje eksperymenty Doctrine było mocno niedopracowane w związku z czym wielokrotnie wzbudzało to moją irytacją. Z tego co pamiętam nie mogłem nawet dowolnie wskazać miejsca generowania modeli tylko było to z góry narzucone. Dlatego między innymi zarzuciłem pomysł „dokooptowania” Doctrine do Kohany.

Na tę chwilę o wiele większe doświadczenie mam z ORM-em Django uważanym za wzorcowy. Jest on o wiele bardziej dopracowany niż wyżej wspomniane PHP-owe odpowiedniki w związku z czym da się z nim w miarę sprawnie pracować. Mimo to pozostaję sceptyczny w kwestii użycia tego typu narzędzi. Opanowanie django-wego ORM-a kosztowało mnie sporo czasu, a do tego nie wyobrażam sobie aby można było efektywnie go używać nie znając wcześniej SQL-a. Przy prostych konstrukcjach jest miło i przyjemnie przy bardziej skomplikowanych użycie obiektów rzutuje przede wszystkim na wydajność, do czego przyczyniają się w dużej mierze bajery typu „lazy loading”. Zmiana ORM-owej konstrukcji w celu optymalizacji i tak poprzedzana jest napisaniem zapytania w SQL więc człowiek nie ma żadnej korzyści z użycia ORM-a, która by rekompensowała nakłady poniesione w celu jego poznania.

O wiele bardziej przemawiają do mnie składacze zapytań SQL takie jak Zend_Db – choć jak większość bibliotek Zenda jest ona przedobrzona. Mając prostą klasę do konstruowania zapytań SQL – taką która ułatwia, nie ogranicza i bynajmniej nie zmusza do uczenia się zamienników w stylu „annotate” zamiast „group by”, wspartą wzorcem projektowym DAO można stworzyć prosty w utrzymaniu, debugowaniu, elastyczny, wcale nie pracochłonny i przede wszystkim odseparowany od warstwy logiki i widoku mechanizm dostępu do bazy danych.

ORM-y mogą być w moim mniemaniu jedynie dodatkami używanymi np. do automatycznego generowania backedu (czytaj panelu administracyjnego), ale w żadnym wypadku nie powinny być jedynym ani nawet głównym sposobem pracy z danymi pobieranymi z bazy danych.

Szablony

Symfony a także Zend Framework, jak równierz Kohana używają natywnych szablonów PHP. Podobnie jak Zyx zgadzam się, że można znaleźć lepszą alternatywę dla warstwy widoku. W odróżnieniu jednak od niego nie stawiałbym na Open Power Template’a, które uważam za trudne i pracochłonne w użyciu (z samej swojej xml-owej natury), a do tego mniej elastyczne i intuicyjne niż kontestowane przez niego Smarty.

System szablonów Smarty w wersji 3 zostało wzbogacone o kilka ciekawych funkcjonalności jak np. dziedziczenie szablonów i możliwość nadpisywania bloków kodu znane z szablonów Django. W przeciwieństwie do ORM-ów uważam, że warto potrudzić się troszkę ze składnią Smarty i pogodzić się z narzutem na wydajność bo w zamian można zyskać szereg usprawnień jakich jesteśmy pozbawieni w przypadku użycia gołych szablonów PHP. Wspomnę tylko wygodniejszą składnię czy zaimplementowany cache, ale też zwiększone bezpieczeństwo wymuszone ograniczoną dostępnością funkcji PHP wewnątrz szablonu czy też encapsulacją zmiennych. Smarty 3 umożliwia też użycie natywnych szablonów PHP ale jest to alternatywa nie ograniczenie.

Do niedawna pracowałem jeszcze z Django w wersji 9.6, w której szablony w stosunku do wersji 1.2 były mocno ograniczone. Obecne szablony Django w wersji 1.x przypominają Smarty (albo na odwrót jak ktoś woli) – z tym, że Smarty 3 są po prostu lepsze. Jestem fanem składni i możliwości języka Python mimo to na polu szablonów PHP u mnie wygrywa.

Reasumując

We wpisie tym nie dążyłem bynajmniej do porównania frameworków między sobą, a już na pewno się do porównywania rozwiązań stosowanych w języku PHP czy Python. Poruszyłem trzy zagadnienia, które w mniejszym lub większym stopniu dotyczą wszystkich z wyżej wymienionych platform. Wspomniałem o tym, że jestem zwolennikiem rozwiązań uniwersalnych ale elastyczność nie może być realizowana kosztem przejrzystości i prostoty. Dałem też do zrozumienia, że mapowanie na siłę relacyjnej bazy danych do postaci obiektów jest raczej wyrazem fundamentalizmu ideologicznego niż pragmatycznym działaniem. Z kolei w użyci systemów szablonów dostrzegłem wiele zalet.