<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Sobakowy Blog</title>
        <link>https://sobak.pl</link>
        <atom:link href="https://sobak.pl/feed" rel="self" type="application/rss+xml" />
        <description>Trochę o wszystkim i wiele o niczym</description>
        <lastBuildDate>Tue, 04 Mar 2025 08:58:04 +0100</lastBuildDate>
        <language>pl</language>
        <ttl>180</ttl>
                <item>
            <title>Scrawler: programmable web crawler</title>
            <link>https://sobak.pl/blog/scrawler-programmable-web-robot</link>
            <pubDate>Sun, 19 May 2019 09:25:00 +0200</pubDate>
                        <category><![CDATA[PHP]]></category>
                        <guid>https://sobak.pl/blog/scrawler-programmable-web-robot</guid>
            <description><![CDATA[<p>A couple of days back my little blog unnoticeably turned nine. Realizing
that what started almost a decade ago as a result of my boredom is still
more or less alive to this day is not only a surprise to me but also a
welcome change from most of my other projects, half of which didn't even
get to the point of publishment.</p>
<p>It was supposed to change at the end of February. I had to collect some data from
quite popular Polish site for further usage and analysis. I started to create
a commandline script to automate the process and had a fleeting thought of
publishing it. However, I realized that combination of one cURL call and
couple of regexes is not complicated enough to deserve a full-blown
project. Once again I started to dream of coming up with some reusable library,
to finally have at least one piece of software put under my name on Packagist.
I was going to drop the conception of scrapper altogether and suddenly epithany
had come. The idea of <strong><a href="http://scrawler.sobak.pl">Scrawler</a></strong> was born.</p>
<p>The general concept was to have the crawler and the library combined. I was
wondering how to design it to make it useful not only for me and this particular
scenario but in a way it could serve as a solid foundation for more cases related
to data scrapping.</p>
<p><strong>Scrawler</strong> became a declarative web robot that you can use to visit almost any
website you want, specify the rules to gather the data by <em>and</em> process it into the
format you want. Of course, it doesn't do all the work for you but it
lets you avoid all of the hassle of handling the HTTP communication, parsing HTML,
respecting <code>robots.txt</code>, getting results in some conveniently abstracted form etc.
It allows you to skip the implementation details and get straight to the point,
like most of libraries do.</p>
<p>Writing a scrapper for a particular data source was easy but making it really
flexible turned that task into a challenge. In fact, it is a case where
good programming practices are actually enforced by the business goal – having
a library for such vague task like web crawling that you cannot further extend
makes it completely useless. Finally, I decided to give myself a week to create
a proof of concept or fail miserably. Seven days and around two thousand lines
of code later I had a script that would go to the given URL, scrap parts of the
contents using provided patterns and map them over the Data Transfer Objects.
I called it a day.</p>
<p>Now, three more months and two internal releases later I feel like it's time
to show my monstrosity to the world. It still isn't very stable, lacks a bunch
of features I planned for the release – but it is mine. I actually did put so
much emphasis to make it mine that even to this day I haven't checked the existing
libraries in that field, not even once. I know they are there. Probably much
better than mine, perhaps they have well-established community and support but
I didn't want to work under their influnce or get intimidated. The whole design
of the extensibility and the flow is entirerly my <del>fault</del> idea.</p>
<p>Scrawler aims to help you with web crawling and data scrapping (hence the name,
don't get <a href="https://en.wiktionary.org/wiki/scrawl">confused</a> by the poor quality of this writeup!). The
process consists of steps like going to the URL, gathering bits of response
to save them and looking for new URLs… just like most other software of this
type. What makes Scrawler special is the abbility to tell it <em>how</em> exactly these
steps should be performed. Each one of them has its own OOP interface plus couple
of built-in implementations you may swap with a single line of code.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-delimiter">&lt;?php</span>

<span class="kl-variable">$scrawler</span> = <span class="kl-keyword">new</span> <span class="kl-symbol kl-class">Configuration</span>()<span class="kl-operator kl-punctuation">;</span>

<span class="kl-variable">$scrawler</span>
    -&gt;<span class="kl-call"><span class="kl-variable kl-property">setOperationName</span></span>(<span class="kl-string kl-single">'Sobakowy Blog'</span>)
    -&gt;<span class="kl-call"><span class="kl-variable kl-property">setBaseUrl</span></span>(<span class="kl-string kl-single">'https://sobak.pl'</span>)
    -&gt;<span class="kl-call"><span class="kl-variable kl-property">addUrlListProvider</span></span>(<span class="kl-keyword">new</span> <span class="kl-symbol kl-class">ArgumentAdvancerUrlListProvider</span>(<span class="kl-string kl-single">'/page/%u'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-number">2</span>))
</span></code></pre>
<p>Do you want to follow every found URL that matches the domain? There's an
implementation waiting for you. To get URLs from external list you have
prepared? I got you covered, just use the other class for finding URLs. You
need to increment a fragment of the URL (e.g. page number) to get more data?
Done. And what if you want to pass every third word of Fifty Shades of Grey
as a search parameter on a site with cute hamsters? Well, conscience clause
stops me from distributing it along Scrawler but hey, you can write your own
class! I can guarantee that you'll fit into twenty lines of code and Scrawler
will still do the rest. The same point applies to all remaining steps of what
Scrawler does. It gives you the tools to do the dirty work quickly.</p>
<p>Given varying output formats like CSV, JSON or any Doctrine-supported database,
Scrawler can be used either as a standalone tool or as a part of the toolchain
where its results are further processed.</p>
<p>There are surely some limitations given Scrawler's early development stage and
the room for improvement is definitely big but that's just one more reason to
release it to the public. Aside from the fact that something what has started
out as an experiment and a time-killer has already proven to be useful for me.</p>
<p>World – meet the Sobak's crawler!</p>
<hr />
<p>Make sure to visit <a href="http://scrawler.sobak.pl">Scrawler's website</a> with the detailed documentation
of available features. Feel free to tinker with it, report issues or submit pull
requests to the <a href="https://github.com/Sobak/scrawler">repository</a>.</p>
]]></description>
        </item>
                <item>
            <title>Sobakowy Blog 2018</title>
            <link>https://sobak.pl/blog/sobak-blog-2018</link>
            <pubDate>Wed, 15 Aug 2018 23:00:00 +0200</pubDate>
                        <category><![CDATA[Informacje]]></category>
                        <guid>https://sobak.pl/blog/sobak-blog-2018</guid>
            <description><![CDATA[<h2>Witaj, 2018 roku!</h2>
<p>Nie planowałem co prawda rozpoczynać sezonu blogowego w połowie sierpnia, ale
szczęśliwie mogę tym razem odezwać się z czymś więcej niż zwyczajowym <em>&quot;No hej,
nie było mnie rok, teraz chyba będę, wpisy nadchodzą — kiedyś…&quot;</em>. Udało mi
się nareszcie skończyć punkt pierwszy, sam szczyt moich list todo od niemal
roku. Z lekką dumą informuję, że znajdujesz się teraz na nowej, odświeżonej
wersji mojej strony.</p>
<p>Prawdopodobnie… nie zauważyłeś różnicy. I nie są to problemy z cache przeglądarki,
a zupełnie zamierzony zabieg. Uznałem, że obecna, czteroletnia forma strony jest
w dalszym ciągu adekwatna i (szczególnie przy moich marnych umiejętnościach
graficznych) nie ma się co silić na rewolucję. Zrobiłem więc ewolucję, która
na drugi rzut oka manifestuje się w najróżniejsze sposoby.</p>
<h2>Żegnaj, WordPressie!</h2>
<p>Po pierwsze, po ponad ośmiu latach i po raz pierwszy od tchnięcia życia w sobak.pl
pożegnałem się z WordPressem. <em>Wspaniałe to było osiem lat, nie zapomnę ich nigdy.</em>
Poważnie mówiąc, nie było źle. Nie potrafię wskazać jednego momentu przełomowego,
przelania się czary goryczy, które spowodowałoby że rozżalony powiedziałbym <em>dość</em>.
Co więc zadecydowało? Suma trzech czynników:</p>
<ul>
<li>
<p>Dawka zdrowej ambicji. Nie wypada szewcowi chodzić w najtańśzych butach z sieciówki
i nie wypada programiście technologii webowych tworzyć swojej wizytówki na pierwszym
lepszym darmowym systemie.</p>
</li>
<li>
<p>Obawy odnośnie bezpieczeństwa. Nie zamierzam tu powielać szeroko rozpowszechnionych
mitów. Rdzeń WordPressa <em>jest</em> bezpieczny, pomimo kilku istotnych wtop z zakresu
procedowania luk bezpieczeństwa, WordPress w swojej bazowej formie skutechnie opiera
się większości ataków.</p>
<p>To, co można powiedzieć o samym gołym produkcie, nie do końca jest jednak prawdą
w wypadku niezliczonych wtyczek. Tak więc w obliczu migracji na VPS-a (o której
słów kika za chwilę) poszukiwałem czegoś, co nie wprowadzi mnie na pole minowe.</p>
</li>
<li>
<p>Potrzeba customizacji. Raz jeszcze przyznać trzeba, że WordPress generalnie rozszerzalny
jest. Twierdzenie inaczej byłoby zanegowaniem faktu istnienia tak wielu wtyczek i motywów
do niego.</p>
<p>Nie oznacza to jednak, że owe rozszerzanie należy w roku 2018 do przyjemnych. Mimo, że
sam jestem autorem kilku z funkcjonalności integrujących się dosyć mocno w poprzednią
wersję witryny, to tworzenie tychże można porównać do szambonurkowania. Wraz z rosnącą
ilością zmian, które chciałem i chciałbym wreszcie wprowadzić na swojej stronie
potrzebowałem czegoś, w czym czułbym się absolutnie swobodnie.</p>
</li>
</ul>
<p>Dlatego też zdecydowałem się na stworzenie własnego backendu w oparciu o framework
Laravel, roboczo określonego nazwą kodową <em>Perception</em> (podziękowania dla <a href="https://olgierd.me">winka</a>, jak
zawsze niezastąpionego w spontanicznym wymyślaniu nazw). Teoretycznie jest to prosta
aplikacja CRUD-owa, więc nie będę za wiele rozpisywał się na temat szczegółów
implementacyjnych. Chcę jedynie wspomnieć o jednej ważnej decyzji powiązanej ze
wspomnianym wyżej poszukiwaniem bezpieczeństwa. Z tego powodu, a także chęci uproszczenia
sobie życia — zarówno w momencie developowania jak i korzystania z platformy — zdecydowałem
się na zupełny brak panelu administracyjnego.</p>
<p>Obecna implementacja czerpie mocno z konceptu generatorów stron statycznych. Źródłem
jej danych są bowiem statyczne pliki tekstowe. W przeciwieństwie jednak do wcześniej
wymienionych generatorów nie są z nich tworzone gotowe pliki HTML, a jedynie przejściowa,
tymczasowa baza danych, która pozwala na wprowadzenie dowolnej ilości dynamiki i w zasadzie
klayczny rozwój aplikacji WWW. Jak wspomniałem jest to też wygodniejsze dla mnie jako użytkownika,
bo oznacza że nie muszę walczyć z dowolnym skryptem, próbując do niego dodać wsparcie Markdowna czy
używać desktopowego edytora tylko po to, aby później wkleić wynik swojej pracy do formularza
na stronie.</p>
<h2>Harder, Better, <em>Faster</em>, Stronger</h2>
<p>Dużo uwagi poświęciłem poprawie wydajności działania strony. Począwszy od przeniesienia
się na szybszy serwer, przez optymalizację backendu, aż po upewnienie się, że wynik jego
pracy może zostać szybko pobrany i obsłużony przez przeglądarkę. Zacząłem od podstaw,
czyli zmniejszenia ilości i rozmiaru tekstu wysyłanego do klienta, wagi obrazków, zapytań
do innych zasobów et ceterra. Wynik całej tej operacji przedstawia skrótowo poniższa galeria.</p>
<div class="gallery">
    <dl>
        <dt>
            <a href='https://sobak.pl/assets/images/perf-2017-google.png'>
                <img src="https://sobak.pl/assets/images/perf-2017-google.png" width="300" height="169" alt="PageSpeed Insights 2017">
            </a>
        </dt>
        <dd>PageSpeed Insights 2017</dd>
    </dl>
    <dl>
        <dt>
            <a href='https://sobak.pl/assets/images/perf-2017-pingdom.png'>
                <img src="https://sobak.pl/assets/images/perf-2017-pingdom.png" width="300" height="169" alt="Pingdom 2017">
            </a>
        </dt>
        <dd>Pingdom 2017</dd>
    </dl>
    <dl>
        <dt>
            <a href='https://sobak.pl/assets/images/perf-2017-gtmetrix.png'>
                <img src="https://sobak.pl/assets/images/perf-2017-gtmetrix.png" width="300" height="169" alt="GTmetrix 2017">
            </a>
        </dt>
        <dd>GTmetrix 2017</dd>
    </dl>
    <dl>
        <dt>
            <a href='https://sobak.pl/assets/images/perf-2018-google.png'>
                <img src="https://sobak.pl/assets/images/perf-2018-google.png" width="300" height="169" alt="PageSpeed Insights 2018">
            </a>
        </dt>
        <dd>PageSpeed Insights 2018</dd>
    </dl>
    <dl>
        <dt>
            <a href='https://sobak.pl/assets/images/perf-2018-pingdom.png'>
                <img src="https://sobak.pl/assets/images/perf-2018-pingdom.png" width="300" height="169" alt="Pingdom 2018">
            </a>
        </dt>
        <dd>Pingdom 2018</dd>
    </dl>
    <dl>
        <dt>
            <a href='https://sobak.pl/assets/images/perf-2018-gtmetrix.png'>
                <img src="https://sobak.pl/assets/images/perf-2018-gtmetrix.png" width="300" height="169" alt="GTmetrix 2018">
            </a>
        </dt>
        <dd>GTmetrix 2018</dd>
    </dl>
</div>
<h2>Pimp my website</h2>
<p>Mimo iż nie zdecydowałem się na całościową wymianę szablonu strony, znalazłem dziesiątki,
jeśli nie setki pomniejszych rzeczy zasługujących na poprawę w obecnym. Poczynając od
rzeczy ogólnych jak zwiększenie kontrastu tekstu, o które prosiliście niedługo po premierze
poprzedniej wersji (4 lata, polecam się), przez poprawę typografii, marginesów, kolorów,
spójności i podrasowanie pojedynczych podstron, jak portfolio. Więcej szczegółów we
<em>wmiaręwykazie</em> zmian na dole wpisu.</p>
<h2>Content is an old, impotent king</h2>
<p>Wydaje mi się, że blog z założenia jest formą treści, która po czasie zaczyna stanowić
pewną historię, więc nie ingerowałem za bardzo w samo sedno byłych wpisów. Nie da się
jednak ukryć że przez ponad osiem lat wiele z nich zaczęło odstawać od reszty strony,
nie tylko stylistycznie lecz nawet w tak jaskrawe sposoby jak użycie już dawno nieużywanej
wtyczki do podświetlania kodu. Poprawiłem też mnóstwo znalezionych po drodze literówek,
niedziałających już dawno linków, a czasem nawet skrajnie nieaktualnych lub błędnych
informacji (to ostatnie jednak robiłem w drodze wyjątku).</p>
<p>W kontekście treści warto też wspomnieć o porzuceniu możliwości komentowania na mojej
stronie. Nad trudnościami technicznymi przeważyły problemy z moderowaniem takiej ilości
tekstu i późniejszym utrzymaniu go w ryzach. Wierzę że komentujący mi wybaczą i z formularza
na dole wpisów zapraszam do kontaktu w dowolnym miejscu wykazanym w zielonym menu (ikona
serduszka na prawo od nagłówka).</p>
<h2>Projekty stare i nowe</h2>
<p>Sporo aktualizacji otrzymało portfolio. Poza poprawkami stylów, drobnymi korektami wielu
z miniaturek, skupiłem się także na aktualizacji zawartej tam treści. Rozbudowałem
opisy wielu z projetków, te które doczekały się nowych wersji zyskały często osobne
podstrony w portfolio, aby oddzielić je od historycznych już tworów. Wreszcie, opisałem
także kilka nowych dzieł.</p>
<p><a href="http://paste.sobak.pl">Pastebin</a>, czyli nowe miejsce do dzielenia się fragmentami kodu źródłowego,
które w obecnej formie powstało kilka nocy temu.</p>
<p><a href="https://sobak.pl/portfolio/blackout">Blackout</a> — <del>opus magnum spierdolenia</del> szczyt projektów bezsensownych,
nieuzasadnionych i z góry skazanych na porażkę, czyli moje starcie z opisywaną już bestią,
bossem projektów legacy — xNovą.</p>
<p><a href="https://sobak.pl/portfolio/codice">Codice</a>, które od ostatniej aktualizacji pozycji w portfolio stało się zupełnie
nowym, otwartoźródłowym projektem. Dawniejsza wersja i delikatny rys historyczny poprzednich
podejść został przeniesiony na osobną podstronę.</p>
<p>I wreszcie zupełnie nowa odsłona <a href="https://sobak.pl/portfolio/sobak-pl">sobak.pl</a> w portfolio. Podobnie jak
wyżej, dawna treść została zachowana w osobnym, podlinkowanym miejscu.</p>
<h2>Co dalej?</h2>
<p>W chwili gdy to piszę, z mojego osobistego todo znika najbardziej rozbudowany i pożądany
punkt od niemal roku. Mogąc się teraz zabrać za inne rzeczy, bez jednoczesnego wywoływania
wyrzutów sumienia, planuję okazać trochę miłości starym, zakurzonym projektom, jak i zająć
się rozwojem kilku nowszych. Stay tuned!</p>
<p>…ach, nie spodziewajcie się jednak w najbliższym czasie nowych tworów wielkoformatowych
pokroju Codice czy codziennych aktualizacji o postępie prac. Stety-niestety gimnazjum
mam już za sobą, a programowanie zawodowe skutecznie ogranicza ilość sił i zwyczajną
ochotę na dalsze zagłębianie się w kod w czasie wolnym. <em>Postaram się</em> jednak aby każdy
miesiąc przyniósł jakąś nową informację o mnie i tym nad czym pracuję.</p>
<h2>W-miarę-wykaz zmian</h2>
<ul>
<li>znacząco poprawiłem czas ładowania strony (porównanie wyżej)</li>
<li>wdrożyłem certyfikat SSL dla sobak.pl i wszystkich subdomen</li>
<li>gorsza wiadomość: ze względu na ograniczony czas wycofałem możliwość
komentowania wpisów</li>
<li>oparłem podświetlanie bloków kodu źródłowego o <a href="http://keylighter.kadet.net">Keylightera</a>
</li>
<li>przerobiłem podgląd wpisów z Twittera, które teraz w większości wypadków
ładują się natychmiastowo</li>
<li>zamieniłem strzałki do paginacji miejscami (następna strona po prawej)</li>
<li>zmodyfikowałem listę linków w sidebarze tak aby ich kolejność była losowa</li>
<li>poprawiłem wyszukiwarkę tak aby poprawnie obsługiwała przycisk <em>Czytaj dalej</em>
dla wpisów, które go posiadają</li>
<li>poprawiłem wyszukiwarkę tak aby pokazywała także normalną paginację, a nie
tylko strzałki po bokach ekranu</li>
<li>dodałem link do profilu na LinkedIn do menu serwisów społecznościowych</li>
<li>otwarcie menu wyszukiwania ustawia teraz kursor w polu formularza</li>
<li>poprawiłem podświetlanie aktywnego elementu menu na wielu podstronach</li>
<li>formularz kontaktowych korzysta teraz z ReCAPTCHA</li>
<li>przywróciłem do życia <a href="http://overdocs.net">overdocs.net</a>
</li>
<li>zmiany wizualne:
<ul>
<li>zwiększyłem kontrast tekstu</li>
<li>poprawiłem style portfolio (w tym responsywność)</li>
<li>poprawiłem style formularza kontaktowego</li>
<li>poprawiłem wyświetlanie menu na urządzeniach mobilnych</li>
<li>poprawiłem typografię tekstu</li>
<li>poprawiłem style nagłówków</li>
<li>poprawiłem style cytatów</li>
<li>poprawiłem style dla wielu wariantów wyświetlania list</li>
<li>poprawiłem wyświetlanie miniaturek w portfolio</li>
<li>dodałem style dla boxów z informacją/ostrzeżeniem</li>
<li>zmniejszyłem wagę fonta dla pogrubionego tekstu</li>
<li>zmniejszyłem delikatnie marginesy dla akapitów</li>
<li>dodałem zawijanie bloków kodu po 450px wysokości na urządzeniach
mobilnych</li>
<li>wiele mniejszych lub zapomnianych poprawek</li>
</ul>
</li>
<li>zmiany w treści (lista niepełna):
<ul>
<li>poprawiłem kilkadziesiąt literówek i miejsc z błędnym formatowaniem
tekstu</li>
<li>poprawiłem wiele niedziałąjacych i przestarzałych linków</li>
<li>zoptymalizowałem wagę niemal wszystkich obrazków wyświetlanych na stronie</li>
<li>połączyłem podobne tagi dla wpisów</li>
<li>portfolio
<ul>
<li>dodałem opis projektu <a href="http://paste.sobak.pl">Pastebin</a>
</li>
<li>dodałem opis projektu <a href="https://sobak.pl/portfolio/blackout">Blackout</a>
</li>
<li>zaktualizowałem opis projektu <a href="https://sobak.pl/portfolio/codice">Codice</a> (opis starej wersji
został wydzielony do osobnej podstrony)</li>
<li>zaktualizowałem opis projektu dla sobak.pl (opis starych wersji,
opartych o WordPressa został wydzielony do osobnej podstrony)</li>
<li>zaktualizowałem opis projektu WordPress Cleaner</li>
</ul>
</li>
<li>wiele pomniejszych poprawek</li>
</ul>
</li>
<li>stats.sobak.pl
<ul>
<li>dodałem podstawowe statystyki z GitHuba</li>
<li>kliknięcie w nazwę serwisu kieruje teraz bezpośrednio do mojego profilu</li>
</ul>
</li>
</ul>
]]></description>
        </item>
                <item>
            <title>Daj Się Poznać 2017: wyjaśnienie</title>
            <link>https://sobak.pl/blog/daj-sie-poznac-2017-wyjasnienie</link>
            <pubDate>Sun, 04 Jun 2017 11:55:00 +0200</pubDate>
                        <category><![CDATA[Informacje]]></category>
                        <guid>https://sobak.pl/blog/daj-sie-poznac-2017-wyjasnienie</guid>
            <description><![CDATA[<p>W związku z ostatnią ciszą na blogu, szczególnie w kontekście zapowiadanego
udziału w konkursie <a href="https://sobak.pl/blog/daj-sie-poznac-czemu-nie">DSP 2017</a>, czuję się zobowiązany do przedstawienia
małych wyjaśnień. Choć blog wskazuje dosyć jasno, że mój udział w zabawie nie
dobiegł do szczęśliwego końca, chciałem o tym z tego miejsca poinformować
oficjalnie. Niestety - sprawy prywatne, a przede wszystkim nieszczególnie
rozsądna organizacja czasu, nie pozwoliły mi na dokończenie wymaganej ilości
wpisów i tym samym zrealizowania moich (początkowo całkiem ambitnych planów).</p>
<p>Zawiedzonych przepraszam, trzymającym kciuki dziękuję, a niepocieszonych pocieszam:
brak udziału w konkursie Maćka Aniserowicza absolutnie nie oznacza końca aktywności
na blogu. Mimo, że sam nie jestem zadowolony z tego potknięcia, wiem że niekiedy się
one zdarzają i zazwyczaj nie przekreślają dalszych przedsięwzięć. Po udziale w <em>DSP</em>
pozostało wszkaże sporo dobrej energii, parę niezłych szkiców, a nade wszystko
lista pomysłów na wpisy, która w dalszym ciągu będzie sukcesywnie aktualizowana.</p>
<p>Do przeczytania wkrótce!</p>
]]></description>
        </item>
                <item>
            <title>Codice: obsługa migracji baz danych dla wtyczek</title>
            <link>https://sobak.pl/blog/codice-obsluga-migracji-baz-danych-dla-wtyczek</link>
            <pubDate>Sun, 23 Apr 2017 21:43:00 +0200</pubDate>
                        <category><![CDATA[PHP]]></category>
                        <guid>https://sobak.pl/blog/codice-obsluga-migracji-baz-danych-dla-wtyczek</guid>
            <description><![CDATA[<p>W <a href="https://sobak.pl/blog/codice-system-wtyczek">poprzednim wpisie</a> przedstawiłem zarys głównego API stojącego
za obsługą rozszerzeń w <a href="https://codice.eu">Codice</a>. Nie jest to oczywiście całość systemu,
wtyczki poza integracją ze <em>rdzeniem</em> za pomocą akcji i filtrów muszą mieć zapewnione
inne możliwości, pozwalające na swobodne operowanie. Jedną z bardziej istotnych
jest wpływ na strukturę bazy danych.</p>
<p>W większości nowoczesnych frameworków dzieje się to za pomocą <em>migracji</em>, czyli
opisanych w ustalony wcześniej sposób procedur kolejnych zmian w bazie danych.
Dotyczy to dodawania, usuwania a także modyfikowania kolumn lub całych tabel.
Przy instalacji aplikacji (także Codice) wszystkie migracje są wykonywane
w kolejności ich definiowania - zapewnia to między innymi bardzo pożądaną
możliwość wersjonowania struktury bazy danych.</p>
<p>Po tym opisie widać już że bardzo wskazane byłoby posiadanie analogicznego
mechanizmu dla wtyczek. Ponieważ migracje powinno definiować się dwustronnie,
można założyć że będą one aplikowane przy instalacji wtyczki, a wszelkie
wprowadzone do struktury bazy zmiany będą cofane przy deinstalacji rozszerzenia
(owa <em>&quot;druga strona&quot;</em> migracji).</p>
<p>By osiągnąć założone cele nie będziemy tworzyć zupełnie autorskiego systemu.
Wszakże Laravel posiada już niemal identyczny mechanizm - brakuje tylko możliwości
przypisania każdej z migracji konkretnej wtyczki. Pierwotnie próbowałem osiągnąć
to za pomocą parametrów przekazywanych do komendy <code>artisan migrate</code>. Dokumentacja
wspomina bowiem o opcji <code>--path</code>, pozwalającej na wykonanie migracji ograniczonych
do jednej lokalizacji w systemie plików. Okazuje się jednak że w ten sposób nie
możemy odwrócić migracji. Zdecydowałem się więc na rozszerzenie domyślnego
repozytorium (w rozumieniu architektury aplikacji obiektowych) migracji o potrzebne
wymagania.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-delimiter">&lt;?php</span>

<span class="kl-keyword">namespace</span> <span class="kl-symbol kl-namespace">Codice\Plugins</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Illuminate\Database\ConnectionResolverInterface</span> <span class="kl-symbol kl-class kl-interface"><span class="kl-keyword">as</span></span> Resolver<span class="kl-operator kl-punctuation">;</span>
<span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Illuminate\Database\Migrations\DatabaseMigrationRepository</span><span class="kl-operator kl-punctuation">;</span>
<span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Illuminate\Database\Migrations\MigrationRepositoryInterface</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">class</span> <span class="kl-symbol kl-class">MigrationRepository</span> <span class="kl-keyword">extends</span> <span class="kl-symbol kl-class">DatabaseMigrationRepository</span> <span class="kl-keyword">implements</span> MigrationRepositoryInterface
{
    <span class="kl-keyword">protected</span> <span class="kl-variable">$plugin</span><span class="kl-operator kl-punctuation">;</span>

    <span class="kl-comment kl-docblock">/**
     * Create a new database migration repository instance.
     *
     * <span class="kl-symbol kl-annotation">@param</span>  Illuminate\Database\ConnectionResolverInterface  <span class="kl-variable">$resolver</span>
     * <span class="kl-symbol kl-annotation">@param</span>  string  <span class="kl-variable">$plugin</span>  Unique application-wide plugin identifier
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">__construct</span>(Resolver <span class="kl-variable">$resolver</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$plugin</span>)
    {
        <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">resolver</span> = <span class="kl-variable">$resolver</span><span class="kl-operator kl-punctuation">;</span>
        <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">plugin</span> = <span class="kl-variable">$plugin</span><span class="kl-operator kl-punctuation">;</span>
        <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">table</span> = <span class="kl-string kl-single">'migrations_plugins'</span><span class="kl-operator kl-punctuation">;</span>

        <span class="kl-symbol kl-class"><span class="kl-keyword">parent</span></span>::<span class="kl-call">__construct</span>(<span class="kl-variable">$resolver</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">table</span>)<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Get the ran migrations.
     *
     * <span class="kl-symbol kl-annotation">@return</span> array
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">getRan</span>()
    {
        <span class="kl-keyword">return</span> <span class="kl-variable">$this</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">table</span></span>()
            -&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'plugin'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">plugin</span>)
            -&gt;<span class="kl-call"><span class="kl-variable kl-property">orderBy</span></span>(<span class="kl-string kl-single">'batch'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'asc'</span>)
            -&gt;<span class="kl-call"><span class="kl-variable kl-property">orderBy</span></span>(<span class="kl-string kl-single">'migration'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'asc'</span>)
            -&gt;<span class="kl-call"><span class="kl-variable kl-property">pluck</span></span>(<span class="kl-string kl-single">'migration'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">all</span></span>()<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Get list of migrations.
     *
     * <span class="kl-symbol kl-annotation">@param</span>  int  <span class="kl-variable">$steps</span>
     * <span class="kl-symbol kl-annotation">@return</span> array
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">getMigrations</span>(<span class="kl-variable">$steps</span>)
    {
        <span class="kl-variable">$query</span> = <span class="kl-variable">$this</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">table</span></span>()
            -&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'plugin'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">plugin</span>)
            -&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'batch'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'&gt;='</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'1'</span>)<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">return</span> <span class="kl-variable">$query</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">orderBy</span></span>(<span class="kl-string kl-single">'migration'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'desc'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">take</span></span>(<span class="kl-variable">$steps</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">get</span></span>()-&gt;<span class="kl-call"><span class="kl-variable kl-property">all</span></span>()<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Get the last migration batch.
     *
     * <span class="kl-symbol kl-annotation">@return</span> array
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">getLast</span>()
    {
        <span class="kl-variable">$query</span> = <span class="kl-variable">$this</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">table</span></span>()
            -&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'plugin'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">plugin</span>)
            -&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'batch'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$this</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">getLastBatchNumber</span></span>())<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">return</span> <span class="kl-variable">$query</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">orderBy</span></span>(<span class="kl-string kl-single">'migration'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'desc'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">get</span></span>()-&gt;<span class="kl-call"><span class="kl-variable kl-property">all</span></span>()<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Log that a migration was run.
     *
     * <span class="kl-symbol kl-annotation">@param</span>  string  <span class="kl-variable">$file</span>
     * <span class="kl-symbol kl-annotation">@param</span>  int     <span class="kl-variable">$batch</span>
     * <span class="kl-symbol kl-annotation">@return</span> void
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">log</span>(<span class="kl-variable">$file</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$batch</span>)
    {
        <span class="kl-variable">$record</span> = [<span class="kl-string kl-single">'plugin'</span> =&gt; <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">plugin</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'migration'</span> =&gt; <span class="kl-variable">$file</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'batch'</span> =&gt; <span class="kl-variable">$batch</span>]<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-variable">$this</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">table</span></span>()-&gt;<span class="kl-call"><span class="kl-variable kl-property">insert</span></span>(<span class="kl-variable">$record</span>)<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Remove a migration from the log.
     *
     * <span class="kl-symbol kl-annotation">@param</span>  object  <span class="kl-variable">$migration</span>
     * <span class="kl-symbol kl-annotation">@return</span> void
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">delete</span>(<span class="kl-variable">$migration</span>)
    {
        <span class="kl-variable">$this</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">table</span></span>()-&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'migration'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$migration</span>-&gt;<span class="kl-variable kl-property">migration</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">delete</span></span>()<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Create the migration repository data store.
     *
     * <span class="kl-symbol kl-annotation">@return</span> void
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">createRepository</span>()
    {
        <span class="kl-variable">$schema</span> = <span class="kl-variable">$this</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">getConnection</span></span>()-&gt;<span class="kl-call"><span class="kl-variable kl-property">getSchemaBuilder</span></span>()<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-variable">$schema</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">create</span></span>(<span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">table</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-keyword">function</span> (<span class="kl-variable">$table</span>) {
            <span class="kl-variable">$table</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">increments</span></span>(<span class="kl-string kl-single">'id'</span>)<span class="kl-operator kl-punctuation">;</span>
            <span class="kl-variable">$table</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">string</span></span>(<span class="kl-string kl-single">'plugin'</span>)<span class="kl-operator kl-punctuation">;</span>
            <span class="kl-variable">$table</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">string</span></span>(<span class="kl-string kl-single">'migration'</span>)<span class="kl-operator kl-punctuation">;</span>
            <span class="kl-variable">$table</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">integer</span></span>(<span class="kl-string kl-single">'batch'</span>)<span class="kl-operator kl-punctuation">;</span>
        })<span class="kl-operator kl-punctuation">;</span>
    }
}
</span></code></pre>
<p>Nadpisywane są wyłącznie niektóre metody, dokładnie tak aby po przekazaniu do
konstruktora klasy identyfikatora danej wtyczki, wykonywać operacje na powiązanych
z nią migracjach. Ich wywoływanie przebiega niemal analogicznie do klasycznych
migracji wbudowanych w Laravela, informacji zasięgnąłem oczywiście buszując w
jego źródłach (i zdecydowanie polecam to rozwiązanie, <em>Larwę</em> za wiele rzeczy
można słusznie skrytykować, ale w kodzie naprawdę łatwo się połapać).</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-comment">// ...</span>

<span class="kl-comment">// Metoda pomocnicza.</span>
<span class="kl-comment">// Używamy wbudowanego migratora, przekazujemy jedynie własne repozytorium.</span>
<span class="kl-keyword">protected</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">prepareMigrator</span>(<span class="kl-variable">$identifier</span>)
{
    <span class="kl-variable">$repository</span> = <span class="kl-keyword">new</span> <span class="kl-symbol kl-class">MigrationRepository</span>(<span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">app</span>[<span class="kl-string kl-single">'db'</span>]<span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$identifier</span>)<span class="kl-operator kl-punctuation">;</span>

    <span class="kl-keyword">if</span> (!<span class="kl-variable">$repository</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">repositoryExists</span></span>()) {
        <span class="kl-variable">$repository</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">createRepository</span></span>()<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-keyword">return</span> <span class="kl-keyword">new</span> <span class="kl-symbol kl-class">Migrator</span>(<span class="kl-variable">$repository</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">app</span>[<span class="kl-string kl-single">'db'</span>]<span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">app</span>[<span class="kl-string kl-single">'files'</span>])<span class="kl-operator kl-punctuation">;</span>
}

<span class="kl-comment">// ...</span>

<span class="kl-comment">// Przy instalacji wtyczki</span>
<span class="kl-variable">$migrator</span> = <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-call">prepareMigrator</span>(<span class="kl-variable">$identifier</span>)<span class="kl-operator kl-punctuation">;</span>
<span class="kl-variable">$migrator</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">run</span></span>([<span class="kl-variable">$plugin</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">path</span></span>(<span class="kl-string kl-single">'migrations'</span>)])<span class="kl-operator kl-punctuation">;</span>

<span class="kl-comment">// I analogicznie przy jej odinstalowywaniu</span>
<span class="kl-variable">$migrator</span> = <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-call">prepareMigrator</span>(<span class="kl-variable">$identifier</span>)<span class="kl-operator kl-punctuation">;</span>
<span class="kl-variable">$migrator</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">rollback</span></span>([<span class="kl-variable">$plugin</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">path</span></span>(<span class="kl-string kl-single">'migrations'</span>)])<span class="kl-operator kl-punctuation">;</span>
</span></code></pre>
<p>Jak widać jest to naprawdę nieskomplikowane zadanie. Nie dajcie się zmylić ilości
kodu, większość z niego to delikatnie zmodyfikowane metody klasy rodzica. Co
ważniejsze, kryje się za tym życiowa lekcja.</p>
<blockquote>
<p>Nie wynajduj koła od nowa, zwłaszcza jeśli masz już cały zestaw narzędzi w postaci
frameworka. Poświęć chwilę na zgłębienie jego działania, zaoszczędzisz wiele czasu
i bólu głowy.</p>
<p>~Sobak, gdzieś w tym momencie</p>
</blockquote>
<p>Pozdrawiam!</p>
<hr>
<p>Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu
Codice. Po więcej szczegółów zapraszam do <a href="https://sobak.pl/blog/codice-zarzadzanie-zadaniami-i-notatnik-online">pierwszego wpisu</a> oraz na
<a href="https://codice.eu">codice.eu</a>. Poza planowaną od dawna serią jest to też realizacja
wymogów konkursu <em>&quot;Daj Się Poznać&quot;</em>. Więcej o nim możecie przeczytać w
<a href="https://sobak.pl/blog/daj-sie-poznac-czemu-nie">tym wpisie</a>. Po nowe materiały poświęcone tworzeniu projektu Codice
zapraszam w <strong>każdy wtorek</strong>. Ponadto więcej wpisów, poświęconych temu projektowi
lub ogólnej tematyce bloga przeczytacie <strong>co sobotę</strong>. Miejmy nadzieję, że tym
razem już zgodnie z harmonogramem!</p>
]]></description>
        </item>
                <item>
            <title>Nginx: mapowanie ruchu z portu na subdomenę</title>
            <link>https://sobak.pl/blog/nginx-mapowanie-ruchu-z-portu-na-subdomene</link>
            <pubDate>Sun, 16 Apr 2017 22:54:00 +0200</pubDate>
                        <category><![CDATA[Technologia]]></category>
                        <guid>https://sobak.pl/blog/nginx-mapowanie-ruchu-z-portu-na-subdomene</guid>
            <description><![CDATA[<p>Stosunkowo często możemy spotkać się z sytuacją, w której aplikacja działająca
przez sieć udostępnia w sobie wbudowany serwer WWW nadający na określonym porcie.
Problem rozpoczyna się w sytuacji, gdy jest to nasza jedyna opcja, a mimo to
chcielibyśmy mieć jakiś bardziej przyjazny adres w postaci np. subdomeny.</p>
<p>Dziś na szybko zademonstruję konfigurację hosta w serwerze nginx właśnie tak,
aby przechwytywał on ruch występujący na określonym porcie - w tym wypadku na
przykładzie Gitea (społecznościowego forku Gogs, lekkiego serwisu w stylu
GitHub do samodzielnego hostowania). Wystarczy nam wiedzieć, że domyślnie
jest on dostępny na porcie <code>3000</code>, a naszym celem jest subdomena <code>git</code>
(na potrzeby wpisu załóżmy że będzie to <code>git.sobak.pl</code>). W celu przekierowania
ruchu wykorzystamy jedno z powszechnych zastosowań serwera nginx, znane
jako <em>reverse proxy</em>.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-plaintext">server {
	listen 80;

	server_name git.sobak.pl;

	location / {
		proxy_pass http://localhost:3000;
	}
}
</span></code></pre>
<p>Tak, tylko tyle i aż tyle. W wypadku gdy korzystamy z HTTPS, zmieniamy drugą
linijkę na <code>listen 443</code>, czyli odpowiedni dla tego rodzaju komunikacji port.
Podany plik prawdopodobnie będziecie musieli zapisać w lokalizacji
<code>/etc/nginx/sites-available/git.sobak.pl</code> i zsymlinkować z <code>sites-enabled</code>,
czyli tak jak dla każdego innego hosta w nginxie. Pamiętajcie o zmianie
przykładowych wartości subdomeny i portu - pamiętajcie że to rozwiązanie
absolutnie nie ogranicza się do tej jednej aplikacji lecz stanowi ogólny
schemat postępowania.</p>
<hr>
<p>Powyższy wpis jest realizacją wymogów konkursu <em>&quot;Daj Się Poznać&quot;</em>. Więcej o nim możecie
przeczytać w <a href="https://sobak.pl/blog/daj-sie-poznac-czemu-nie">tym wpisie</a>. Po nowe materiały poświęcone tworzeniu projektu
<a href="https://codice.eu">Codice</a>, z którym startuję w konkursie, zapraszam w <strong>każdy wtorek</strong>.
Ponadto więcej wpisów, poświęconych temu projektowi lub ogólnej tematyce bloga
przeczytacie <strong>co sobotę</strong>. Miejmy nadzieję że publikacja dwóch wpisów w niedzielę
jest tylko niewinnym wypadkiem przy pracy ;)</p>
]]></description>
        </item>
                <item>
            <title>Codice: system wtyczek</title>
            <link>https://sobak.pl/blog/codice-system-wtyczek</link>
            <pubDate>Sun, 16 Apr 2017 22:36:00 +0200</pubDate>
                        <category><![CDATA[PHP]]></category>
                        <guid>https://sobak.pl/blog/codice-system-wtyczek</guid>
            <description><![CDATA[<p>Jednym z kluczowych elementów, nad którym pracowałem (i nadal pracuję) przy
tworzeniu <a href="https://codice.eu">Codice</a> jest wbudowana obsługa dla rozszerzeń. Zagadnienie,
na którym nie raz zjadałem sobie zęby lub, co jeszcze gorsze, cierpiałem przez
odkładanie jego implementacji <em>na wieczne nigdy</em>. Do dziś po dysku wala mi się
jakaś wczesna wersja Codice… w bodajże 3 odmianach, różniąch się drobnymi
detalami. Niby drobiazg, ale nie chcę nawet zaczynać opisywania tego, jak
męczące jest równoległe utrzymanie każdego z wariantów.</p>
<p>Skoro próbowałem już tak wiele razy i obecne podejście do Codice miało być
wreszcie tym właściwym, musiałem wreszcie zmierzyć się z tym zagadnieniem.
Jednocześnie musiałem dopuścić do głosu nieco pragmatyzmu i wybrać podejście,
które będzie adekwatne do mojego doświadczenia w tym temacie i na którym,
mówiąc wprost, nie polegnę po raz kolejny.</p>
<p>W tym wpisie przedstawię najbardziej podstawowe elementy systemu wtyczek w moim
projekcie, mianowicie <em>akcje</em> i <em>filtry</em>. Wielu z Was zapewne zna ten koncept
z innych aplikacji (na przykład WordPressa). Wszystko opiera się na zdefiniowanych
w <em>rdzeniu</em> hookach, do których możemy podpiąć własne zdarzenia po to, by wykonały
się w określonej sytuacji. Pierwszy z brzegu przykład: rejestrujemy akcję dodającą
wiadomość powitalną dla zdarzenia (hooka) <code>user.created</code> - utworzenia nowego
użytkownika.</p>
<p>Innym sposobem rozszerzalności mogą być filtry - ponownie, zdefiniowane w <em>rdzeniu</em>
miejsca, w których istotne wartości możemy później modyfikować (filtrować) za pomocą
podpiętych callbacków. W przypadku Codice jest to między innymi zapytanie używane
przy wyszukiwaniu notatek. Wszystko z myślą o wtyczkach, które chciałyby je zmienić
np. tak aby wykorzystywało inne pola lub też wyszukiwanie pełnotekstowe z MySQL.</p>
<p>Nie będę więcej przedłużał, przejdę do prezentowania stojącego za tym kodu. Jeśli
jakiś koncept wciąż nie jest jasny, nie martwcie się, na samym końcu zalinkuję
do testów dla tej części kodu, wydaje mi się, że one powinny idealnie zobrazować
zamysł i planowany sposób ich wykorzystania.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-delimiter">&lt;?php</span>

<span class="kl-keyword">namespace</span> <span class="kl-symbol kl-namespace">Codice\Support\Traits</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Exception</span><span class="kl-operator kl-punctuation">;</span>
<span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Log</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">trait</span> Hookable
{
    <span class="kl-comment kl-docblock">/**
     * <span class="kl-symbol kl-annotation">@var</span> <span class="kl-symbol kl-class">array</span> Holds all currently registered hookables
     */</span>
    <span class="kl-keyword">protected</span> <span class="kl-keyword">static</span> <span class="kl-variable">$hookables</span><span class="kl-operator kl-punctuation">;</span>

    <span class="kl-comment kl-docblock">/**
     * Register new hookable for given hook.
     *
     * <span class="kl-symbol kl-annotation">@param</span> string <span class="kl-variable">$hook</span> Name of the hook
     * <span class="kl-symbol kl-annotation">@param</span> string <span class="kl-variable">$hookableName</span> Name of the hookable, must be unique within a hookable of given type
     * <span class="kl-symbol kl-annotation">@param</span> callable <span class="kl-variable">$callable</span> Callable containing code to run
     * <span class="kl-symbol kl-annotation">@param</span> int <span class="kl-variable">$priority</span> Order of calling hookables, when priority is equal order is undefined
     * <span class="kl-symbol kl-annotation">@return</span> bool
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">register</span>(<span class="kl-variable">$hook</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$hookableName</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-keyword">callable</span> <span class="kl-variable">$callable</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$priority</span> = <span class="kl-number">10</span>)
    {
        <span class="kl-variable">$hookableType</span> = <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-call">getHookableType</span>()<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">if</span> (<span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-call">isRegistered</span>(<span class="kl-variable">$hook</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$hookableName</span>)) {
            <span class="kl-symbol kl-class">Log</span>::<span class="kl-call">warning</span>(<span class="kl-string kl-double">&quot;<span class="kl-variable">$hookableType</span> '<span class="kl-variable">$hookableName</span>' was already registered within '<span class="kl-variable">$hook</span>' hook and has been overwritten.&quot;</span>)<span class="kl-operator kl-punctuation">;</span>
        }

        <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-variable">$hookables</span>[<span class="kl-variable">$hook</span>][<span class="kl-variable">$hookableName</span>] = [
            <span class="kl-string kl-single">'priority'</span> =&gt; <span class="kl-variable">$priority</span><span class="kl-operator kl-punctuation">,</span>
            <span class="kl-string kl-single">'callable'</span> =&gt; <span class="kl-variable">$callable</span><span class="kl-operator kl-punctuation">,</span>
        ]<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">return</span> <span class="kl-constant">true</span><span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Check whether hookable of given name has been registered within a specified hook.
     *
     * <span class="kl-symbol kl-annotation">@param</span> string <span class="kl-variable">$hook</span> Name of the hook
     * <span class="kl-symbol kl-annotation">@param</span> string <span class="kl-variable">$hookableName</span> Name of the hookable within a hook
     * <span class="kl-symbol kl-annotation">@return</span> bool
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">isRegistered</span>(<span class="kl-variable">$hook</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$hookableName</span>)
    {
        <span class="kl-keyword">return</span> <span class="kl-keyword">isset</span>(<span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-variable">$hookables</span>[<span class="kl-variable">$hook</span>][<span class="kl-variable">$hookableName</span>])<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Deregister hookable from given hook.
     *
     * Deregistration must happen before call() method is run to have an effect.
     *
     * <span class="kl-symbol kl-annotation">@param</span> string <span class="kl-variable">$hook</span> Name of the hook
     * <span class="kl-symbol kl-annotation">@param</span> string <span class="kl-variable">$hookableName</span> Name of the hookable
     * <span class="kl-symbol kl-annotation">@return</span> bool
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">deregister</span>(<span class="kl-variable">$hook</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$hookableName</span>)
    {
        <span class="kl-keyword">unset</span>(<span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-variable">$hookables</span>[<span class="kl-variable">$hook</span>][<span class="kl-variable">$hookableName</span>])<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">return</span> <span class="kl-constant">true</span><span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Get (sorted) list of all hookables assigned to a hook.
     *
     * <span class="kl-symbol kl-annotation">@param</span> string <span class="kl-variable">$hook</span> Name of the hook
     * <span class="kl-symbol kl-annotation">@return</span> array
     */</span>
    <span class="kl-keyword">private</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">getHookables</span>(<span class="kl-variable">$hook</span>)
    {
        <span class="kl-variable">$hookables</span> = <span class="kl-keyword">isset</span>(<span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-variable">$hookables</span>[<span class="kl-variable">$hook</span>]) ? <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-variable">$hookables</span>[<span class="kl-variable">$hook</span>] : []<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">return</span> <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-call">sortHookables</span>(<span class="kl-variable">$hookables</span>)<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Sort hookables by their priority.
     *
     * <span class="kl-symbol kl-annotation">@param</span> array <span class="kl-variable">$hookables</span> Array of unsorted hookables
     * <span class="kl-symbol kl-annotation">@return</span> array
     */</span>
    <span class="kl-keyword">protected</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">sortHookables</span>(<span class="kl-variable">$hookables</span>)
    {
        <span class="kl-call">usort</span>(<span class="kl-variable">$hookables</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-keyword">function</span> (<span class="kl-variable">$a</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$b</span>) {
            <span class="kl-keyword">if</span> (<span class="kl-variable">$a</span>[<span class="kl-string kl-single">'priority'</span>] == <span class="kl-variable">$b</span>[<span class="kl-string kl-single">'priority'</span>]) {
                <span class="kl-keyword">return</span> <span class="kl-number">0</span><span class="kl-operator kl-punctuation">;</span>
            }

            <span class="kl-keyword">return</span> <span class="kl-variable">$a</span>[<span class="kl-string kl-single">'priority'</span>] &lt; <span class="kl-variable">$b</span>[<span class="kl-string kl-single">'priority'</span>] ? <span class="kl-number">-1</span> : <span class="kl-number">1</span><span class="kl-operator kl-punctuation">;</span>
        })<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">return</span> <span class="kl-variable">$hookables</span><span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Return human readable name of hookable
     *
     * <span class="kl-symbol kl-annotation">@throws</span> Exception
     * <span class="kl-symbol kl-annotation">@return</span> string
     */</span>
    <span class="kl-keyword">protected</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">getHookableType</span>()
    {
        <span class="kl-keyword">throw</span> <span class="kl-keyword">new</span> <span class="kl-symbol kl-class">Exception</span>(<span class="kl-string kl-single">'getHookableType() must be implemented'</span>)<span class="kl-operator kl-punctuation">;</span>
    }
}
</span></code></pre>
<p>Zaczynamy od… traita? Dokładnie tak. Ponieważ główną różnicą między akcjami
a filtrami jest to, że pierwsze nie mogą zwracać wartości, a drugie muszą to
robić, możemy śmiało stwierdzić że spora część kodu będzie się powtarzać.
Dlatego też wszystkie wspólne funkcjonalności, takie jak rejestrowanie
konkretnych zdarzeń, wywoływanie ich czy potrzebne w obu wypadkach metody
pomocnicze zostały umieszone w jednym miejscu, jak pokazano wyżej.</p>
<p>Jak sami zobaczycie, dzięki temu kod odpowiedzialny za działanie akcji i filtrów
będzie naprawdę krótki i prosty. Jest to kolejno:</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-delimiter">&lt;?php</span>

<span class="kl-keyword">namespace</span> <span class="kl-symbol kl-namespace">Codice\Plugins</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Codice\Support\Traits\Hookable</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">class</span> <span class="kl-symbol kl-class">Action</span>
{
    <span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Hookable</span><span class="kl-operator kl-punctuation">;</span>

    <span class="kl-comment kl-docblock">/**
     * Call all actions registered for a given hook.
     *
     * <span class="kl-symbol kl-annotation">@param</span> string <span class="kl-variable">$hook</span> Name of the hook
     * <span class="kl-symbol kl-annotation">@param</span> array <span class="kl-variable">$parameters</span> Parameters which will be passed to callback
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">call</span>(<span class="kl-variable">$hook</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-keyword">array</span> <span class="kl-variable">$parameters</span> = [])
    {
        <span class="kl-variable">$actions</span> = <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-call">getHookables</span>(<span class="kl-variable">$hook</span>)<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">foreach</span> (<span class="kl-variable">$actions</span> <span class="kl-keyword">as</span> <span class="kl-variable">$action</span>) {
            <span class="kl-call">call_user_func</span>(<span class="kl-variable">$action</span>[<span class="kl-string kl-single">'callable'</span>]<span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$parameters</span>)<span class="kl-operator kl-punctuation">;</span>
        }
    }

    <span class="kl-comment kl-docblock">/**
     * <span class="kl-symbol kl-annotation">@inheritdoc</span>
     */</span>
    <span class="kl-keyword">protected</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">getHookableType</span>()
    {
        <span class="kl-keyword">return</span> <span class="kl-string kl-single">'Action'</span><span class="kl-operator kl-punctuation">;</span>
    }
}
</span></code></pre>
<p>oraz</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-delimiter">&lt;?php</span>

<span class="kl-keyword">namespace</span> <span class="kl-symbol kl-namespace">Codice\Plugins</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Codice\Support\Traits\Hookable</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">class</span> <span class="kl-symbol kl-class">Filter</span>
{
    <span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Hookable</span><span class="kl-operator kl-punctuation">;</span>

    <span class="kl-comment kl-docblock">/**
     * Call all filters registered for a given hook.
     *
     * <span class="kl-symbol kl-annotation">@param</span> string <span class="kl-variable">$hook</span> Name of the hook
     * <span class="kl-symbol kl-annotation">@param</span> mixed <span class="kl-variable">$value</span> Value to run filters on
     * <span class="kl-symbol kl-annotation">@param</span> array <span class="kl-variable">$parameters</span> Parameters which will be passed to callback
     * <span class="kl-symbol kl-annotation">@return</span> mixed Filtered value
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">call</span>(<span class="kl-variable">$hook</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$value</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-keyword">array</span> <span class="kl-variable">$parameters</span> = [])
    {
        <span class="kl-variable">$filters</span> = <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-call">getHookables</span>(<span class="kl-variable">$hook</span>)<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">foreach</span> (<span class="kl-variable">$filters</span> <span class="kl-keyword">as</span> <span class="kl-variable">$filter</span>) {
            <span class="kl-variable">$value</span> = <span class="kl-call">call_user_func</span>(<span class="kl-variable">$filter</span>[<span class="kl-string kl-single">'callable'</span>]<span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$value</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$parameters</span>)<span class="kl-operator kl-punctuation">;</span>
        }

        <span class="kl-keyword">return</span> <span class="kl-variable">$value</span><span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * <span class="kl-symbol kl-annotation">@inheritdoc</span>
     */</span>
    <span class="kl-keyword">protected</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">getHookableType</span>()
    {
        <span class="kl-keyword">return</span> <span class="kl-string kl-single">'Filter'</span><span class="kl-operator kl-punctuation">;</span>
    }
}
</span></code></pre>
<p>Jak na dłoni widać teraz różnice w działaniu obu <em>hookowalnych</em> (zaczepialnych?)
elementów. Tak więc raz jeszcze - w obu wypadkach wykonujemy wszystkie
zarejestrowane (przez pluginy bądź samą aplikację) zdarzenia, czyniąc to według
określonego priorytetu. Akcje po prostu wykonują kod, a filtry kolejno modyfikują
przekazaną wartość.</p>
<p>Jak już mówiłem, najszybszym sposobem na zobaczenie ich w akcji jest spojrzenie na
testy - osobno dla <a href="https://github.com/getcodice/codice/blob/master/tests/Unit/PluginActionsTest.php">akcji</a> oraz <a href="https://github.com/getcodice/codice/blob/master/tests/Unit/PluginFiltersTest.php">filtrów</a>. Przykłady
użycia podstawowego API do wtyczek przez samą aplikację znajdują się <a href="https://github.com/getcodice/codice/tree/master/app/Core">tutaj</a>,
nazwy plików powinny być raczej jasne ;)</p>
<hr>
<p>Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu
Codice. Po więcej szczegółów zapraszam do <a href="https://sobak.pl/blog/codice-zarzadzanie-zadaniami-i-notatnik-online">pierwszego wpisu</a> oraz na
<a href="https://codice.eu">codice.eu</a>. Poza planowaną od dawna serią jest to też realizacja
wymogów konkursu <em>&quot;Daj Się Poznać&quot;</em>. Więcej o nim możecie przeczytać w
<a href="https://sobak.pl/blog/daj-sie-poznac-czemu-nie">tym wpisie</a>. Po nowe materiały poświęcone tworzeniu projektu Codice
zapraszam w <strong>każdy wtorek</strong>. Ponadto więcej wpisów, poświęconych temu projektowi
lub ogólnej tematyce bloga przeczytacie <strong>co sobotę</strong>. Miejmy nadzieję, że tym
razem już zgodnie z harmonogramem!</p>
]]></description>
        </item>
                <item>
            <title>Codice: weryfikacja właściciela zasobu (uprawnień użytkownika)</title>
            <link>https://sobak.pl/blog/codice-weryfikacja-wlasciciela-zasobu-uprawnien-uzytkownika</link>
            <pubDate>Wed, 22 Mar 2017 08:28:00 +0100</pubDate>
                        <category><![CDATA[PHP]]></category>
                        <guid>https://sobak.pl/blog/codice-weryfikacja-wlasciciela-zasobu-uprawnien-uzytkownika</guid>
            <description><![CDATA[<p>Z kilku przyczyn delikatnie rozjechał mi się harmonogram cotygodniowy, ale czas
nadrobić straty. Poprzednio opisywałem doświadczenia zebrane podczas projektowania
procesu instalacji dla <a href="http://codice.eu">Codice</a>. Zajmiemy się teraz kolejnym zagadnieniem,
które mimo iż oparte na przykładzie tego konkretnego projektu, jest jednym z
podstawowych zagadnień dla każdej aplikacji <a href="https://pl.wikipedia.org/wiki/CRUD">CRUD</a>. Zachęcam do tego aby ten,
jak i kolejne wpisy traktować jako zwyczajne poradniki dla piszących w Laravelu,
z nutką historii w tle.</p>
<p>Za każdym razem gdy piszemy aplikację, której instancja ma być używana równolegle
przez więcej niż jedną osobę, pojawia się kwestia weryfikacji właściciela zasobu.
Prościej mówiąc - sprawdzenia czy obecnie zalogowany użytkownik ma uprawnienia
do wyświetlenia/edycji/usunięcia danego rekordu. Na pierwszy rzut oka sprawa
nie należy do skomplikowanych:</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-variable">$note</span> = <span class="kl-symbol kl-class">Note</span>::<span class="kl-call">where</span>(<span class="kl-string kl-single">'id'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$request</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">get</span></span>(<span class="kl-string kl-single">'id'</span>))-&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'user_id'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-symbol kl-class">Auth</span>::<span class="kl-call">id</span>())-&gt;<span class="kl-call"><span class="kl-variable kl-property">get</span></span>()<span class="kl-operator kl-punctuation">;</span>
</span></code></pre>
<p>Na przykładzie notatek, pobieramy rekord o ID przekazanym w adresie i zaznaczamy
że ID przypisanego użytkownika musi być równe identyfikatorowi obecnie zalogowanego
(metoda pomocnicza <code>id()</code> z fasady <code>Auth</code>). Przyjmijmy też że naszym celem jest
usunięcie wybranej notatki. W razie błędu przekierujemy zaś na inną stronę, na
której wyświetlimy czytelny dla użytkownika komunikat. Bardziej kompletny kod
oparty o metodzie przedstawionej powyżej mógłby wyglądać tak:</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-variable">$note</span> = <span class="kl-symbol kl-class">Note</span>::<span class="kl-call">where</span>(<span class="kl-string kl-single">'id'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-variable">$request</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">get</span></span>(<span class="kl-string kl-single">'id'</span>))-&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'user_id'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-symbol kl-class">Auth</span>::<span class="kl-call">id</span>())-&gt;<span class="kl-call"><span class="kl-variable kl-property">get</span></span>()<span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">if</span> (<span class="kl-variable">$note</span> === <span class="kl-constant">null</span>) {
    <span class="kl-keyword">return</span> <span class="kl-call">redirect</span>(<span class="kl-string kl-single">'/'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">with</span></span>(<span class="kl-string kl-single">'error'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'Nie jesteś właścicielem podanej notatki'</span>)<span class="kl-operator kl-punctuation">;</span>
}

<span class="kl-variable">$note</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">delete</span></span>()<span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">return</span> <span class="kl-call">redirect</span>(<span class="kl-string kl-single">'/'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">with</span></span>(<span class="kl-string kl-single">'success'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'Notatka usunięta'</span>)<span class="kl-operator kl-punctuation">;</span>
</span></code></pre>
<p>W wypadku gdy nie uda się odnaleźć pasującego rekordu otrzymujemy <code>null</code>. Wiemy
wtedy że podana notatka nie istnieje lub istnieje, ale należy do kogoś innego
(wymagamy spełnienia obu warunków), możemy więc cofnąć użytownika i wyświetlić
mu odpowiednią wiadomość.</p>
<p>Na dobrą sprawę wpis można by zakończyć już w tym miejscu. Warto jednak spojrzeć
bardziej perspektywicznie. Podany fragment kodu będzie się przecież powtarzał
także dla innych operacji CRUD (np. edycji lub odczytu), nasza aplikacja może
też zarządzać więcej niż jednym rodzajem danych (na przykładzie Codice - notatkami
oraz etykietami). W efekcie otrzymamy brzydką i niewygodną w utrzymaniu powtarzalność
kodu. Zastanówmy się jak można to poprawić.</p>
<p>Rozsądne wydaje się skorzystanie z metody <code>find()</code>, która przyjmuje za konwencję,
że polem, po którym będziemy indeksować tabelę jest <code>id</code>. Możemy połączyć to także
z możliwością opisaną w dokumentacji Laravela jako <em>query scopes</em>.
Do naszego modelu dodajemy:</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">scopeMine</span>(<span class="kl-variable">$query</span>)
{
    <span class="kl-keyword">return</span> <span class="kl-variable">$query</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'user_id'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'='</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-symbol kl-class">Auth</span>::<span class="kl-call">id</span>())<span class="kl-operator kl-punctuation">;</span>
}
</span></code></pre>
<p>a wynikowo naszą metodę kontrolera redukujemy do postaci</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-variable">$note</span> = <span class="kl-symbol kl-class">Note</span>::<span class="kl-call">mine</span>()-&gt;<span class="kl-call"><span class="kl-variable kl-property">find</span></span>(<span class="kl-variable">$request</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">get</span></span>(<span class="kl-string kl-single">'id'</span>))<span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">if</span> (<span class="kl-variable">$note</span> === <span class="kl-constant">null</span>) {
    <span class="kl-keyword">return</span> <span class="kl-call">redirect</span>(<span class="kl-string kl-single">'/'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">with</span></span>(<span class="kl-string kl-single">'error'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'Nie jesteś właścicielem podanej notatki'</span>)<span class="kl-operator kl-punctuation">;</span>
}

<span class="kl-variable">$note</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">delete</span></span>()<span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">return</span> <span class="kl-call">redirect</span>(<span class="kl-string kl-single">'/'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">with</span></span>(<span class="kl-string kl-single">'success'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'Notatka usunięta'</span>)<span class="kl-operator kl-punctuation">;</span>
</span></code></pre>
<p>Jest lepiej. Powtarzalna część zapytania została przeniesiona do modelu (oraz
konwencji używanej przez ORM Laravela). Wciąż jednak widzimy niezmienną część
logiki, którą także przydałoby się <em>wypchnąć</em> na zewnątrz, w jakieś wspólne miejsce.</p>
<p>W tym celu możemy wykorzystać wyjątki, bazując zresztą na logice używanej przez
wbudowaną metodę <code>findOrFail()</code>. Do naszego modelu dodajemy kolejną metodę
pomocniczą:</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-keyword">public</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">findMine</span>(<span class="kl-variable">$id</span>)
{
    <span class="kl-variable">$note</span> = <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-call">mine</span>()-&gt;<span class="kl-call"><span class="kl-variable kl-property">find</span></span>(<span class="kl-variable">$id</span>)<span class="kl-operator kl-punctuation">;</span>

    <span class="kl-keyword">if</span> (!<span class="kl-variable">$note</span>) {
        <span class="kl-keyword">throw</span> <span class="kl-keyword">new</span> <span class="kl-symbol kl-class">NoteNotFoundException</span><span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-keyword">return</span> <span class="kl-variable">$note</span><span class="kl-operator kl-punctuation">;</span>
}
</span></code></pre>
<p>W obrębie aplikacji tworzymy wyjątek <code>NoteNotFoundException</code> i importujemy
go w modelu.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-delimiter">&lt;?php</span>

<span class="kl-keyword">namespace</span> <span class="kl-symbol kl-namespace">App\Exceptions</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">class</span> <span class="kl-symbol kl-class">NoteNotFoundException</span> <span class="kl-keyword">extends</span> <span class="kl-symbol kl-class">\Exception</span> {}
</span></code></pre>
<p>Logikę obsługi błędów przenosimy natomiast do handlera wyjątków, edytując
metodę <code>render()</code> w klasie <code>AppExceptionsHandler</code>.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">render</span>(<span class="kl-variable">$request</span><span class="kl-operator kl-punctuation">,</span> Exception <span class="kl-variable">$e</span>)
{
    <span class="kl-keyword">if</span> (<span class="kl-variable">$e</span> <span class="kl-keyword">instanceof</span> NoteNotFoundException) {
        <span class="kl-keyword">return</span> <span class="kl-symbol kl-class">Redirect</span>::<span class="kl-call">route</span>(<span class="kl-string kl-single">'index'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">with</span></span>(<span class="kl-string kl-single">'message'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'Nie jesteś właścicielem podanej notatki`);
    }

    // reszta domyślnego kodu, np. ModelNotFoundException
}
</span></span></code></pre>
<p>Teraz kod użyty w kontrolerze powinien być naprawdę krótki i satysfakcjonujący:</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-variable">$note</span> = <span class="kl-symbol kl-class">Note</span>::<span class="kl-call">findMine</span>(<span class="kl-variable">$request</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">get</span></span>(<span class="kl-string kl-single">'id'</span>))<span class="kl-operator kl-punctuation">;</span>

<span class="kl-variable">$note</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">delete</span></span>()<span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">return</span> <span class="kl-call">redirect</span>(<span class="kl-string kl-single">'/'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">with</span></span>(<span class="kl-string kl-single">'success'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'Notatka usunięta'</span>)<span class="kl-operator kl-punctuation">;</span>
</span></code></pre>
<p>Jest dobrze? Tak. Może być lepiej? Oczywiście! Wady rozwiązania ujawnią się przy
jakiejkolwiek większej skali. Pozostając przy Codice - ten sam kod musiałby być
zduplikowany dla notatek i etykiet - dwa różne wyjątki, dwa bloki obsługujące go
w handlerze, analogiczne modyfikacje w obu modelach.</p>
<p>Skupimy się więc na wyłączeniu części wspólnych. Wyeliminujemy potrzebę tworzenia
indywidualnych wyjątków dla każdego rodzaju danych, a metody wymagane dla każdego
z modeli przeniesiemy do traita.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-delimiter">&lt;?php</span>

<span class="kl-keyword">namespace</span> <span class="kl-symbol kl-namespace">Codice\Support\Traits</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Auth</span><span class="kl-operator kl-punctuation">;</span>
<span class="kl-keyword">use</span> <span class="kl-symbol kl-class">Illuminate\Http\Exceptions\HttpResponseException</span><span class="kl-operator kl-punctuation">;</span>

<span class="kl-keyword">trait</span> Owned
{
    <span class="kl-comment kl-docblock">/**
     * Find owned model (limited to the current user).
     *
     * <span class="kl-symbol kl-annotation">@param</span>  int <span class="kl-variable">$id</span> Model ID
     * <span class="kl-symbol kl-annotation">@return</span> static
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">static</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">findMine</span>(<span class="kl-variable">$id</span>)
    {
        <span class="kl-variable">$model</span> = <span class="kl-symbol kl-class"><span class="kl-keyword">self</span></span>::<span class="kl-call">mine</span>()-&gt;<span class="kl-call"><span class="kl-variable kl-property">find</span></span>(<span class="kl-variable">$id</span>)<span class="kl-operator kl-punctuation">;</span>

        <span class="kl-keyword">if</span> (!<span class="kl-variable">$model</span>) {
            <span class="kl-variable">$modelLangFile</span> = <span class="kl-call">array_reverse</span>(<span class="kl-call">explode</span>(<span class="kl-string kl-single">'<span class="kl-operator kl-escape">\\</span>'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-call">get_called_class</span>()))[<span class="kl-number">0</span>]<span class="kl-operator kl-punctuation">;</span>

            <span class="kl-keyword">throw</span> <span class="kl-keyword">new</span> <span class="kl-symbol kl-class">HttpResponseException</span>(<span class="kl-call">redirect</span>()-&gt;<span class="kl-call"><span class="kl-variable kl-property">route</span></span>(<span class="kl-string kl-single">'index'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">with</span></span>([
                <span class="kl-string kl-single">'message'</span> =&gt; <span class="kl-call">trans</span>(<span class="kl-string kl-double">&quot;<span class="kl-variable">$modelLangFile</span>.not-found&quot;</span>)<span class="kl-operator kl-punctuation">,</span>
                <span class="kl-string kl-single">'message_type'</span> =&gt; <span class="kl-string kl-single">'danger'</span><span class="kl-operator kl-punctuation">,</span>
            ]))<span class="kl-operator kl-punctuation">;</span>
        }

        <span class="kl-keyword">return</span> <span class="kl-variable">$model</span><span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Query scope for getting owned models (limited to the current user).
     *
     * <span class="kl-symbol kl-annotation">@param</span> <span class="kl-variable">$query</span> Illuminate\Database\QueryBuilder
     * <span class="kl-symbol kl-annotation">@return</span> Illuminate\Database\QueryBuilder
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">scopeMine</span>(<span class="kl-variable">$query</span>)
    {
        <span class="kl-keyword">return</span> <span class="kl-variable">$query</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">where</span></span>(<span class="kl-string kl-single">'user_id'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'='</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-symbol kl-class">Auth</span>::<span class="kl-call">id</span>())<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment kl-docblock">/**
     * Define relationship to the User owning the model.
     */</span>
    <span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">user</span>()
    {
        <span class="kl-keyword">return</span> <span class="kl-variable">$this</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">belongsTo</span></span>(<span class="kl-string kl-single">'CodiceUser'</span>)<span class="kl-operator kl-punctuation">;</span>
    }
}
</span></code></pre>
<p>W tym wypadku obsługa po stronie kontrolera wygląda dokładnie tak, jak pokazano
wyżej, jednak cała implementacja logiki dla określonego modelu ogranicza się
do <code>use Owned</code>, bez żadnego powtarzania schematycznego kodu. Dodatkowym klasom
wyjątków również możemy już podziękować, korzystamy z wbudowanego <code>HttpResponseException</code>,
więc <code>AppExceptionsHandler</code> może zostać przywrócone do wcześniejszej postaci.
Dodatkowo mamy też własny scope <code>mine()</code> (przydatny przy innych rodzajach
zapytań niż wybranie pojedynczego rekordu po ID) oraz zdefiniowaną relację
do modelu użytkownika.</p>
<p>Ciekawostką jest hack w metodzie <code>findMine()</code>, który pobiera nazwę modelu
wywołującego metodę i na tej podstawie buduje klucz do pliku z tłumaczeniami
(w celu zróżnicowania komunikatów błędów, np. <em>&quot;nie znaleziono notatki&quot;</em> vs
<em>&quot;nie znaleziono etykiety&quot;</em>).</p>
<p>Przedstawiłem kilka rożnych metod, skracając nieco drogę, przez którą ten
kawałek kodu przeszedł w moim własnym projekcie. Zainteresowanych sprawdzeniem
faktycznego kodu mogę odesłać do historii zmian w
<a href="https://github.com/getcodice/codice/commits/master/app/Http/Controllers/NoteController.php">kontrolerze notatek</a> oraz oczywiście <a href="https://github.com/getcodice/codice/blob/master/app/Support/Traits/Owned.php">traita <code>Owned</code></a></p>
<hr>
<p>Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu
Codice. Po więcej szczegółów zapraszam do <a href="https://sobak.pl/blog/codice-zarzadzanie-zadaniami-i-notatnik-online">pierwszego wpisu</a> oraz na
<a href="http://codice.eu">codice.eu</a>. Poza planowaną od dawna serią jest to też realizacja
wymogów konkursu <em>&quot;Daj Się Poznać&quot;</em>. Więcej o nim możecie przeczytać w
<a href="https://sobak.pl/blog/daj-sie-poznac-czemu-nie">tym wpisie</a>. Po nowe materiały poświęcone tworzeniu projektu Codice
zapraszam w <strong>każdy wtorek</strong>. Ponadto więcej wpisów, poświęconych temu projektowi
lub ogólnej tematyce bloga przeczytacie <strong>co sobotę</strong>.</p>
]]></description>
        </item>
                <item>
            <title>Laravel i podpowiedzi IDE? To możliwe</title>
            <link>https://sobak.pl/blog/laravel-i-podpowiedzi-ide-to-mozliwe</link>
            <pubDate>Sat, 18 Mar 2017 13:15:00 +0100</pubDate>
                        <category><![CDATA[PHP]]></category>
                        <guid>https://sobak.pl/blog/laravel-i-podpowiedzi-ide-to-mozliwe</guid>
            <description><![CDATA[<p>Nie ukrywajmy - <em>magii</em> w Laravelu jest dużo. Na tyle dużo, że można ją kochać
bądź nienawidzić - nie wyobrażam sobie stanu pośredniego. Abstrahując zupełnie
od tego, czy takie rozwiązanie jest słuszne, trzeba mu wytknąć jedną, definitywną
wadę. Zintegrowane środowiska programistyczne (IDE) w kontakcie z nią po prostu
wariują.</p>
<p>Nie ma problemu dopóki korzystamy z prostych edytorów tekstowych typu Sublime
Text, Atom czy Visual Studio Code. Więszkość z nich albo w ogóle nie dostarcza
podpowiedzi dla kodu PHP lub są one mało &quot;inteligentne&quot;. Bazują na prostym
słowniku użytych symboli (nazw funkcji, klas czy zmiennych) i wyszukiwarce.
Tutaj rysuje się jedna z głównych różnić pomiędzy edytorem, a IDE - środowisko
programistyczne stara się &quot;zrozumieć kod&quot;. Za pomocą różnych zaawansowanych
parserów stara się przewidzieć zachowanie faktycznego interpretera PHP i jako
podpowiedzi podaje nam tylko to, czego możemy faktycznie użyć w danym kontekście,
bez spowodowania błędu. Nie dostaniemy w podpowiedzi zmiennej, która jest
niedostępna w danym zasięgu, ponieważ PHP jako interpreter też nie byłby jej
w stanie użyć.</p>
<p>Mimo całej swojej &quot;inteligencji&quot; IDE nie odwzorowują jednak w stu procentach
zachowania języka - złożoność takiego programu byłaby naprawdę ogromna,
a prędkość działania niezadowalająca. Dlatego występują sytuacje, w których
dany kod w rzeczywistości działa, a IDE po prostu się &quot;gubi&quot;. Nie przetwarza
wszystkich dynamicznych sytuacji obecnych na przykład przy użyciu wspomnianej
<em>magii</em> języka.</p>
<p>Z opisaną sytuacją spotkał się na pewno każdy, kto próbował używać projektu
napisanego w Laravelu w którymkolwiek z popularnych środowisk programistycznych
dla PHP, na przykład PhpStormie. Zaznaczę że mówimy tu głównie o wykorzystaniu
fasad (w rozumieniu Laravela), których można unikać - promuje je jednak sama
dokumentacja. W praktyce program pokazuje tak wiele błędów, że żeby nie
rozpraszać się nimi trzeba by wyłączyć praktycznie wszystkie podpowiedzi
(lub tzw. inspekcje). Przyznacie jednak, że pozbawienie IDE całej jego
&quot;inteligencji&quot; i tym samym zrobienie z niego zwyczajnego edytora trochę
mija się z celem.</p>
<p>Czy jest na to jakieś rozwiązanie? Oczywiście.</p>
<p>W przypadku Laravela możemy użyć bardzo popularnej paczki <a href="https://github.com/barryvdh/laravel-ide-helper">laravel-ide-helper</a>.
Analizuje ona kod frameworka i generuje plik będący swego rodzaju &quot;mapą&quot; pomiędzy
fasadami, prostymi interfejsami używanymi przez programistę, a normalnymi klasami,
których wewnętrznie używa framework, komunikując te dwie warstwy za pomocą sporej
ilości skomplikowanych odwołań i opisywanej <em>magii</em>.</p>
<p>Tak wygenerowany plik, obecny w katalogu projektu wystarcza aby więszkość środowisk
potrafiła nadążyć za sztuczkami zastosowanymi w Laravelu. Nie tylko przestała
pokazywać błędy, ale faktycznie zaczęła pomagać, na przykład podpowiadając
dostępne metody i ich parametry.</p>
<p>Zaczynamy od zainstalowania paczki przy użyciu <a href="https://getcomposer.org">Composera</a>. Myślę że
dowolnej osobie używającej Laravela czy innego nowoczesnego frameworka PHP nie
trzeba tłumaczyć obsługi tego narzędzia.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-shell"><span class="kl-call">composer</span> require <span class="kl-symbol kl-parameter">--dev</span> barryvdh/laravel-ide-helper
</span></code></pre>
<p>Zwróćcie uwagę że instalujemy paczkę jako zależność deweloperską. Skutkuje to
tym, że w środowisku produkcyjnym w ogóle nie będzie ona pobierana. Teraz
przechodzimy do kolejnego kroku, czyli zarejestrowania Service Providera,
który komunikuje paczkę z frameworkiem. Tutaj też odbiegniemy nieco od
standardowej metody, czyli dodania wpisu w <code>config/app.php</code> i rejestracji
Service Providera dokonamy warunkowo. Dlaczego? Ponieważ w przeciwnym razie,
w środowisku produkcyjnym (a więc pominięciu instalacji tej paczki) framework
próbowałby użyć nieistniejącej klasy. Dlatego też otworzymy plik
<code>app/providers/AppServiceProvider.php</code> i w metodzie <code>register()</code> dodamy
następujący kod:</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">register</span>()
{
    <span class="kl-keyword">if</span> (<span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">app</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">environment</span></span>() !== <span class="kl-string kl-single">'production'</span>) {
        <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">app</span>-&gt;<span class="kl-call"><span class="kl-variable kl-property">register</span></span>(<span class="kl-symbol kl-class">Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider</span>::<span class="kl-keyword">class</span>)<span class="kl-operator kl-punctuation">;</span>
    }
    <span class="kl-comment">// ...</span>
}
</span></code></pre>
<p>Dzięki temu framework nie będzie próbował rejestrować Service Providera na produkcji.</p>
<p>Następnie możemy już skorzystać z udostępnionych nam komend.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-shell"><span class="kl-call">php</span> artisan clear-compiled
<span class="kl-call">php</span> artisan ide-helper:generate
<span class="kl-call">php</span> artisan optimize
</span></code></pre>
<p>Paczka udostępnia nieco więcej opcji, włącznie z generowaniem dokumentacji dla
naszych modeli, jednak nie są one generowane domyślnie. Po szczegóły raz jeszcze
odsyłam do oficjalnego <a href="https://github.com/barryvdh/laravel-ide-helper">repozytorium</a></p>
<p>W głównym katalogu projektu powstanie plik <code>_ide_helper.php</code>. Nie musimy go
nigdzie dołączać, w tym momencie wszystko powinno już działać. Ów plik
zdecydowanie dodajmy do ignorowanych przez Gita, o ile z niego korzystamy
(a przecież wszyscy to robimy, prawda?). W najprostszym wariancie dodajemy
linię <code>/_ide_helper.php</code> do <code>.gitignore</code>. O tym, dlaczego niekoniecznie
jest to najlepszy pomysł, postaram się napisać już niedługo ;)</p>
<hr>
<p>Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu
Codice. Po więcej szczegółów zapraszam do <a href="https://sobak.pl/blog/codice-zarzadzanie-zadaniami-i-notatnik-online">pierwszego wpisu</a> oraz na
<a href="http://codice.eu">codice.eu</a>. Poza planowaną od dawna serią jest to też realizacja
wymogów konkursu <em>&quot;Daj Się Poznać&quot;</em>. Więcej o nim możecie przeczytać w
<a href="https://sobak.pl/blog/daj-sie-poznac-czemu-nie">tym wpisie</a>. Po nowe materiały poświęcone tworzeniu projektu Codice
zapraszam w <strong>każdy wtorek</strong>. Ponadto więcej wpisów, poświęconych temu projektowi
lub ogólnej tematyce bloga przeczytacie <strong>co sobotę</strong>.</p>
]]></description>
        </item>
                <item>
            <title>Codice: instalator webowy dla aplikacji opartej o Laravel</title>
            <link>https://sobak.pl/blog/codice-instalator-webowy-dla-aplikacji-opartej-o-laravel</link>
            <pubDate>Tue, 14 Mar 2017 20:51:00 +0100</pubDate>
                        <category><![CDATA[PHP]]></category>
                        <guid>https://sobak.pl/blog/codice-instalator-webowy-dla-aplikacji-opartej-o-laravel</guid>
            <description><![CDATA[<p>Dośc długo zastanawiałem się czemu mógłbym poświęcić swój pierwszy, faktyczny
wpis poświęcony rozwojowi <a href="https://codice.eu">Codice</a> i problemom jakie musiałem rozwiązać.
Daruję sobie rozpoczynanie od struktury katalogów, tworzenia kontrolerów i podobnych
zadań — zaczniemy od początku, ale z perspektywy <em>użytkownika</em>.</p>
<p>Dość dużo mówi się o tym, jak zmienił się rozwój aplikacji webowych w ostatnich
latach. Kiedyś (czy to patrząc na rok, czy na doświadczenie konkretnego programisty)
prawdpodobnie wszyscy zaczynaliśmy, w wypadku PHP, od kilku plików na krzyż,
w ekstremalnych przypadkach wrzucając jakąś rozpakowaną bibliotekę do osobnego
folderu. Dzisiaj rozwój projektów, zwłaszcza na początku, przypomina budowę
z klocków Lego. Zaczynamy od zainstalowania całego <em>środowiska</em>
programistycznego — serwera (lub programu do obsługi środowisk zwirtualizowanych),
Composera do obsługi zależności PHP, menedżera zależności dla JavaScript popularnego
akurat w danym tygodniu (pun intended ;) ) i dopiero ostatecznie przystępujemy do
pisania faktycznego kodu.</p>
<p>Ze stawianiem już gotowej aplikacji jest całkiem podobnie. Pobieramy kod, a
następnie zdefiniowane w projekcie zależności. Potem jest już z górki — kompilujemy
assety dla frontendu (w końcu to tylko kwestia odpalenia task runnera), wypełniamy
plik konfiguracyjny (lub ustawiamy zmienne środowiskowe), uruchamiamy migracje
dla bazy danych, jeśli nam zależy to wrzucamy do cache co się da — na koniec
tworzymy konto użytkownika i <em>voilà</em>. Proste, prawda?</p>
<p>A no właśnie nie bardzo. Wszystkie z opisanych rozwiązań to ogromny krok naprzód
i ułatwienie dla programistów. Co więcej, przekładające się bezpośrednio na
jakość ostatecznego produktu. Nie przerzucajmy jednak całego procesu ustawiania
środowiska na użytkownika. Mamy dostępnych tyle świetnych technik i narzędzi,
a tymczasem proces instalacji przeciętnego skryptu PHP się uwstecznił. W wypadku
Codice postanowiłem połączyć zdobycze naszych czasów z prostotą instalacji
dawnych skryptów.</p>
<p>Czynności do wykonania możemy podzielić na trzy grupy:</p>
<ul>
<li>wykorzystanie zewnętrznych narzędzi — node.js wraz z <a href="https://yarnpkg.org">Yarnem</a> zarządzającym
zależnościami dla tej technologii i gulpem odpowiedzialnym za przygotowanie
zasobów dla client-side, ale także Composer dla zależności PHP</li>
<li>wypełnienie plików konfiguracyjnych</li>
<li>uruchomienie procesów oskryptowanych w PHP — na przykład migracji dla
struktury bazy danych</li>
</ul>
<p>Pierwszy krok zrealizowałem dzięki udostępnieniu do pobrania wstępnie zbudowanej
paczki plików. W archiwum znajdują się produkcyjne zależności PHP, a także
skompilowane assety (nie ma za to <code>node_modules</code> i innych plików wykorzystywanych
wyłącznie przy developmencie — szanujmy łącza i czas użytkowników).</p>
<p>Resztą zajmuje się już instalator, będący <em>de facto</em> zwyczajnym kontrolerem
Laravela. Aplikację zainstalowaną od tej jeszcze niezainstalowanej postanowiłem
rozróżniać sprawdzajac obecność pliku <code>.env</code>. Ponieważ jednak jest on tworzony
na pewnym etapie instalacji, potrzebny był dodatkowy sposób oznaczenia obecnie
trwającej instalacji (tak, aby po kroku wypełniającym <code>.env</code> nie wyrzuciło nas
z instalatora) — jest to po prostu plik tymczasowy tworzony po wejściu do
instalatora i usuwany na jej ostatnim kroku. Cały proces wygląda następująco:</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-keyword">public</span> <span class="kl-keyword">function</span> <span class="kl-symbol kl-function">__construct</span>()
{
    <span class="kl-keyword">if</span> (<span class="kl-call">file_exists</span>(<span class="kl-call">base_path</span>(<span class="kl-string kl-single">'.env'</span>)) &amp;&amp; !<span class="kl-call">file_exists</span>(<span class="kl-call">storage_path</span>(<span class="kl-string kl-single">'app/.install-pending'</span>))) {
        <span class="kl-variable">$this</span>-&gt;<span class="kl-variable kl-property">denyInstallation</span> = <span class="kl-constant">true</span><span class="kl-operator kl-punctuation">;</span>
        <span class="kl-keyword">return</span> <span class="kl-symbol kl-class">Redirect</span>::<span class="kl-call">route</span>(<span class="kl-string kl-single">'index'</span>)-&gt;<span class="kl-call"><span class="kl-variable kl-property">send</span></span>()<span class="kl-operator kl-punctuation">;</span>
    }

    <span class="kl-comment">// ...</span>
}
</span></code></pre>
<p>Kod znajduje się w konstruktorze więc automatycznie jest wykonywany dla każdego
z kroków (akcji) instalatora. Warto zwrócić uwagę na użycie metody <code>send()</code>,
jest ona wymagana w wypadku próby obsługi żądań HTTP z poziomu konstruktora.</p>
<p>Kolejnym etapem jest sprawdzenie wymagań, tj. obecności rozszerzeń PHP
i zapisywalności określonych katalogów. Nie ma tu żadnej filozofii także
przejdę dalej, do wypełniania pliku <code>.env</code>. Z pozoru proste zadanie, ot
umieszczenie określonego stringa w pliku tekstowym. Jest jednak pewna pułapka,
która swego czasu spędziła mi sen z powiek na ładnych parę godzin…</p>
<p>Problem pojawia się gdy w instalatorze wykorzystujemy jakiekolwiek sesje bądź
ciasteczka (w moim wypadku było to ustawienie języka instalacji). Zarówno sesje
jak i ciasteczka obsługiwane przez Laravela są szyfrowane przy użyciu klucza,
tzw. <code>APP_KEY</code>. Jest on indywidualny dla każdej aplikacji, a więc przechowywany
właśnie w pliku <code>.env</code>. Ten jednak nie istnieje na samym początku instalacji.
Co wtedy robi Laravel? Spójrzmy na plik <code>config/app.php</code>:</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-string kl-single">'key'</span> =&gt; <span class="kl-call">env</span>(<span class="kl-string kl-single">'APP_KEY'</span><span class="kl-operator kl-punctuation">,</span> <span class="kl-string kl-single">'SomeRandomStringSomeRandomString'</span>)<span class="kl-operator kl-punctuation">,</span>
</span></code></pre>
<p>W wypadku gdy zmienna nie istnieje, klucz jest ustawiany na wartość domyślną,
podaną w drugim parametrze funkcji. To jest właśnie klucz szyfrujący ciastka
i sesje na poczatku instalacji… aż do momentu gdy, wypełniając <code>.env</code>,
wstawimy do niego nowy klucz, który własnie wygenerowaliśmy. Skutek można sobie
łatwo wyobrazić - dane w nich zapisane stają się niemożliwe do odczytania.
Mając tę wiedzę spokojnie możecie wymyślić kilka różnych obejść problemu — ja
zdecydowałem się doczepić aktualnie wybrany język do adresu, na który przekierowuję
po wypełnieniu pliku konfiguracyjnego. W kolejnym kroku mogę go z tego adresu
odczytać i ponownie zapisać do sesji.</p>
<p>W ten sposób doszliśmy do kolejnej kwestii, która na pierwszy rzut oka może
wydawać się problematyczna — uruchomienie migracji z poziomu skryptu PHP.
Domyślnie jest to bowiem komenda <code>artisana</code>, a dokumentacja zdaje się nie
wspominać o alternatywnych drogach. Również wymóg użycia <code>exec()</code> dla zwykłej
instalacji wydaje się przesadny, wszkaże wiele hostingów nadal blokuje tę
i podobne funkcje. Tym razem jednak rozwiązanie jest wyjątkowo proste,
a z pomocą przychodzi nam <code>Artisan::call()</code>, który wykonuje kod danej komendy
bez jakiegokolwiek używania warstwy linii komend.</p>
<pre class="keylighter" lang="en"><code><span class="kl-language kl-php"><span class="kl-symbol kl-class">Artisan</span>::<span class="kl-call">call</span>(<span class="kl-string kl-single">'migrate'</span><span class="kl-operator kl-punctuation">,</span> [<span class="kl-string kl-single">'--force'</span> =&gt; <span class="kl-constant">true</span>])<span class="kl-operator kl-punctuation">;</span>
</span></code></pre>
<p>Pamiętajmy o użyciu opcji <code>--force</code>, jest ona wymagana aby migracje mogły być
uruchomione w środowisku produkcyjnym.</p>
<p>W wypadku Codice to już praktycznie koniec, pozostało tylko utworzenie konta dla
użytkownika i dodanie mu notatki powitalnej. W swoich aplikacjach możecie
oczywiście wykonać wszystkie inne czynności związane z instalacją, tak aby
jak najbardziej ułatwić życie Waszych użytkowników końcowych. Naprawdę nie
ma potrzeby wymagać od nich znajomości typowo programistycznych narzędzi ;)
Ponadto, stosując przedstawione tutaj rozwiązanie nie blokujemy żadnej z
dotychczasowych dróg instalacji. Nic nie stoi na przeszkodzie aby pobrać
źródła (lub sklonować repozytorium) i wszystkie czynności wykonać ręcznie.</p>
<p>Na koniec, wszystkim zainteresowanym dokładnym działaniem instalatora polecam
zacząć przeglądanie kodu od <a href="https://github.com/getcodice/codice/blob/master/app/Http/Controllers/InstallController.php">tego kontrolera</a>. Aby zamieszać
jeszcze troszkę, dodam że Codice obsługuje też instalację z poziomu CLI, dla osób
które chciałyby postawić całą instalację bez opuszczania terminala bądź szukają
wygodnej drogi na zautomatyzowanie procesu. Odpowiedzialny za to kod znajdziecie
<a href="https://github.com/getcodice/codice/blob/master/app/Console/Commands/Install.php">w tym miejscu</a>.</p>
<hr>
<p>Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu Codice.
Po więcej szczegółów zapraszam do <a href="https://sobak.pl/blog/codice-zarzadzanie-zadaniami-i-notatnik-online">pierwszego wpisu</a> oraz na
<a href="https://codice.eu">codice.eu</a>. Poza planowaną od dawna serią jest to też realizacja wymogów
konkursu <em>&quot;Daj Się Poznać&quot;</em>. Więcej o nim możecie przeczytać w <a href="https://sobak.pl/blog/daj-sie-poznac-czemu-nie">tym wpisie</a>.
Po nowe materiały poświęcone tworzeniu projektu Codice zapraszam w <strong>każdy wtorek</strong>.
Ponadto więcej wpisów, poświęconych temu projektowi lub ogólnej tematyce bloga
przeczytacie <strong>co sobotę</strong>.</p>
]]></description>
        </item>
                <item>
            <title>Codice: zarządzanie zadaniami i notatnik online</title>
            <link>https://sobak.pl/blog/codice-zarzadzanie-zadaniami-i-notatnik-online</link>
            <pubDate>Sun, 12 Mar 2017 01:52:00 +0100</pubDate>
                        <category><![CDATA[PHP]]></category>
                        <guid>https://sobak.pl/blog/codice-zarzadzanie-zadaniami-i-notatnik-online</guid>
            <description><![CDATA[<p>W życiu każdego projektu (i jego twórcy) przychodzi taki moment, kiedy należy
wyjść z wygodnego stanu <em>wiecznej wersji beta</em> i niezobowiązującego <em>dłubania</em>
w kodzie przy okazji wolnych wieczorów. Wyjść z kodem do ludzi, liczyć się z tym,
że ktoś może go rzeczywiście użyć (kto by pomyślał?), a nawet skrytykować.
Uznałem, że ten etap nadszedł zarówno dla mnie, jak i mojego dziecka ochrzczonego
mianem <strong>Codice</strong>.</p>
<p>Mówi się że w programowaniu istnieją jedynie dwie trudne rzeczy. Unieważnianie
cache i nazewnictwo. Nazwa dla projektu (zarówno w tej wersji i jego wcześniejszych
iteracji) powstała już lata temu i z włoskiego oznacza <em>kod</em> lub <em>kodeks</em>.
Nawiązanie do kodu jest tak naprawdę miłym zbiegiem okoliczności, który powoduje
że wydaje się znacznie bardziej przemyślana niż była w rzeczywistości. Kodeks zaś?
Pamięć podpowiada mi że miałem na myśli zbiory notatek Leonarda da Vinci, które
nazwano właśnie kodeksami. Czytałem o nich jako dziecko, zafascynowany różnorodnością
i ilością zapisków sporządzanych przez <em>signor Leonardo</em>, i jak to dziecko - pomyślałem
że <em>też bym tak chciał!</em>. Szczęśliwie, myśl ta przetrwała przez kilka lat
i stwierdziłem że mógłbym ją wcielić w życie korzystając z technologii,
których się uczyłem.</p>
<p>Długim słowem wstępu przechodzimy do przedstawienia projektu <strong>notatnika</strong>
i aplikacji do <strong>zarządzania zadaniami</strong> online. Potrzeba gromadzenia
i wykorzystywania przeróżnych informacji jest chyba wspólna dla każdego z nas.
Pomieszczenie, a zwłaszcza późniejsze odnalezienie istotnych danych z całego
szumu którym jesteśmy bombardowani jest niełatwe. Dlaczego więc i w tym
wypadku nie wspomóc się technologią?</p>
<p>Pierwsza wersja Codice (wtedy jeszcze pod inną nazwą) powstała w 2011 roku.
Była tak naprawdę jednym olbrzymim switchem, którego eksperymentalnie umieściłem
w jednym pliku - bo dlaczego nie? Niespełna 300 linii PHP plus jeden arkusz stylów
zapewniał mi miejsce do dodawania notatek, którym opcjonalnie mogłem przypisać
termin wykonania, a później oznaczyć taką notatkę (czy już wówczas zadanie) jako
wykonaną. Zainteresowanych archeologią zapraszam (ostrzegając jednocześnie) do
<a href="https://gist.github.com/Sobak/bbed81e08945193140c86fbaadc3e0fa">zobaczenia kodu</a> na własne oczy.</p>
<p>Co zmieniło się przez te 6 lat? Przepisywałem projekt kilka razy, nieraz odkładając
go do szuflady na długie miesiące. Na pewno jednak nie zmieniło się założenie,
obecna wersja działa w naprawdę zbliżony sposób.</p>
<p>W pewnym momencie zdałem sobie sprawę że w tym właśnie leży siła. Banalnie prosta
idea, do której można dobudować bardzo wiele - do tego użyteczna. Zrozumiałem że
jest to coś, czemu jestem gotów poświęcić sporo pracy, a przy okazji podzielić
się efektem ze światem.</p>
<p>Dzisiaj Codice jest aplikacją MVC, zbudowaną w oparciu o framework <a href="https://laravel.com">Laravel</a>,
która, pod względem napisanego kodu, powiększyła się niemal… pięćdziesięciokrotnie.
Tyle mniej więcej wypuściłem z pod palców, w wolnych chwilach od grudnia 2015, kiedy
raz jeszcze postanowiłem wymienić fundamenty i większość kodu.</p>
<p>Codice, o wciąż niezmiennej koncepcji, doczekał się nowego designu, wielu dodatkowych
możliwości, nieco lepszej architektury a także protego systemu wtyczek, który powinien
zapewnić mu pewną elastyczność na najbliższe lata i moje plany z nim związane.</p>
<p>Na obecnym etapie można chyba zaryzykować nazwanie go produktem używalnym, czego
powinienem być żywym dowodem, korzystając z niego na co dzień od około pół roku.
W dalszym ciągu udziela mi się oczywiście syndrom malarza i widzę w nim
nieskończenie wiele rzeczy, które można by zrobić lepiej. No ale cóż,
w myśl tego, co napisałem we wstępie czas zmierzyć się z odbiorem na zewnątrz
i wykorzystać go jako niezaprzeczalną szansę na zebranie kilku opinii, przed
osiągnięciem wersji stabilnej.</p>
<p>Swoje postępy na tej drodze będę opisywał od tej chwili na blogu. Według
obecnego planu wpisy powinny pojawiać się w <strong>każdy wtorek</strong>, co najmniej
do połowy maja.</p>
<p>Jeśli zaintrygowałem kogoś już w tym momencie, zapraszam na
<a href="http://codice.eu">stronę projektu</a>. Można poczytać sobie dokumentację jeśli ktoś ma
problemy z zasypianiem lub po prostu pobrać paczkę i spróbować coś zepsuć ;)</p>
<hr>
<p>Powyższy wpis poza planowanym już dawno przedstawieniem Codice i wstępem do
serii, w której postaram się przybliżyć jego możliwości, pokazując przy tym
jak rozwiązałem co ciekawsze problemy, jest też realizacją wymogów konkursu
<em>&quot;Daj Się Poznać&quot;</em>. Więcej o nim możecie przeczytać w moim
<a href="https://sobak.pl/blog/daj-sie-poznac-czemu-nie">wczorajszym wpisie</a>. Oznacza to też, że co najmniej do połowy maja
<strong>co sobotę</strong> będą pojawiać się kolejne materiały, tym razem poświęcone
niekoniecznie rozwojowi Codice, ale również związane z tematyką bloga.</p>
]]></description>
        </item>
            </channel>
</rss>
