<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>webbricks &#187; DRY</title>
	<atom:link href="http://blog.grzegorzpawlik.com/tag/dry/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.grzegorzpawlik.com</link>
	<description>Doświadczenie, to coś, co zdobywamy tuż po chwili w której było nam potrzebne ...</description>
	<lastBuildDate>Tue, 07 Feb 2012 10:09:24 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Fat model, skinny controller &#8211; przykład</title>
		<link>http://blog.grzegorzpawlik.com/2010/05/fat-model-skinny-controller-przyklad/</link>
		<comments>http://blog.grzegorzpawlik.com/2010/05/fat-model-skinny-controller-przyklad/#comments</comments>
		<pubDate>Fri, 21 May 2010 04:00:00 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[Agile]]></category>
		<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[Controller]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[model]]></category>

		<guid isPermaLink="false">http://blog.grzegorzpawlik.com/?p=975</guid>
		<description><![CDATA[Niedawno przy projekcie trafiliśmy na ciekawy problem. Chciałbym się podzielić z Wami tym czego się nauczyliśmy. Problem przedstawię w bardzo uproszczonej formie, bo trudno bez machania rękoma przy zabazgranej tablicy wyjaśnić go w całości. Problem Mamy formularz dodawania kosztów, który &#8230; <a href="http://blog.grzegorzpawlik.com/2010/05/fat-model-skinny-controller-przyklad/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Niedawno przy projekcie trafiliśmy na ciekawy problem. Chciałbym się podzielić z Wami tym czego się nauczyliśmy.</p>
<p>Problem przedstawię w bardzo uproszczonej formie, bo trudno bez machania rękoma przy zabazgranej tablicy wyjaśnić go w całości.</p>
<h4>Problem</h4>
<p>Mamy formularz dodawania kosztów, który jest dość specyficzny. Można dodać &#8220;łysy koszt&#8221; i wtedy pojawia się formularz z wyborem firmy i innymi atrybutami. Można jednak z widoku konkretnej firmy wybrać opcję &#8220;dodaj koszt&#8221; &#8211; w takim wypadku lista wyboru firm się nie pojawia.</p>
<p><a href="http://blog.grzegorzpawlik.com/wp-content/uploads/2010/05/Costs_blank.png" rel="lightbox[975]" title="przykładowe formularze"><img src="http://blog.grzegorzpawlik.com/wp-content/uploads/2010/05/Costs_blank.png" alt="" title="przykładowe formularze" width="500" height="123" class="alignnone size-full wp-image-976" /></a></p>
<p>Dość naturalne jest załatwić to kontrolerem. Są różne możliwości, jedną z nich jest takie zdefiniowanie akcji:</p>
<pre name="code" class="php">
function add($entryId=null) {
   if(!empty($this->data)){
       $this->Cost->save($this->data);
       // redirect w jakieś przyjemne okolice
   }
   if(is_null($entryId)){
      $this->set("entry", $this->Cost->Entry->read(null, $entryId);
   }else{
      $this->set("entries", $this->Cost->Entry->find("list");
   }
}
</pre>
<p>W widoku wiadomo &#8211; jeśli jest $entry, wyświetlimy $entry["Entry"]["name"] i w jakimś ukrytym inpucie wrzucimy id, jeśli istnieje $entries &#8211; wyświetlimy selecta.</p>
<p>Na razie w kontrolerze wygląda to wystarczająco zgrabnie, żeby tego nie ruszać. Ale jeśli znajdziesz się w sytuacji, gdy ilość przypadków jest coraz większa &#8211; możesz się zorientować, że dodajesz kolejne argumenty do metody add i rozbudowujesz tą metodę. W takiej sytuacji może Ci pomóc takie podejście.</p>
<h4>Propozycja rozwiązania</h4>
<p>Po pierwsze olej zwykłe parametry funkcji, a zacznij używać parametrów &#8220;named&#8221;. Czyli zamiast:<br />
costs/add/1/3/45/23<br />
costs/add/2///22<br />
będziesz miał<br />
costs/add/cost_id:1/entry_id:3/param3:45/param4:23<br />
costs/add/cost_id:2/param4:22</p>
<p>Po drugie &#8211; przenieś logikę działania do modelu. np:</p>
<pre name="code" class="php">
//model Cost
function giveMeProperData($params){
   if(isset($params["cost_id")){
      // do sth
      return $some_array;
   }
   if(isset($params["entry_id")){
      // do sth
      return $other_array;
   }
   return array();
}
</pre>
<p>Kontroler uprość do granic możliwości:</p>
<pre name="code" class="php">
function add($entryId=null) {
   if(!empty($this->data)){
       $this->Cost->save($this->data);
       // redirect w jakieś przyjemne okolice
   }
   $this->data = $this->Cost->giveMeProperData($this->params["named"];
}
</pre>
<p>A w widoku w zależności od tego w jaki sposób wygląda $this->data renderuj widok.<br />
Jeśli w tablicy jest &#8220;entry_id&#8221; to wyświetl zawartość $this->data["Entry"]["name"], a wartość $this->data["Entry"]["id"] przypisz do jakiegoś ukrytego pola. Itd.</p>
<h4>Co zyskujesz?</h4>
<ol>
<li>Kontroler robi to co do niego należy. Przestaje go interesować przypadek właśnie rozpatrywany. Jego interesuje tylko czy został przesłany formularz. Jeśli tak &#8211; spróbuj zapisać i przekierować, jeśli nie &#8211; pobierz dla niego dane i wyświetl (przekaż do widoku)</li>
<li>Testowanie modeli, choć nie tak proste, jest o niebo łatwiejsze od testowania kontrolerów i widoków &#8211; możesz pokryć ważną część aplikacji testami. Możesz testować, czy metoda giveMeProperData() zwraca to, czego się spodziewasz dla konkretnych argumentów.</li>
<li>Jeśli pokryjesz tą metodę testami będziesz mógł w komfortowych warunkach refaktoryzować ten kod &#8211; jak się pojawią powtórzenia wyodrębnisz wspólne części itd. Gdyby to siedziała w kontrolerze &#8211; nikt nie miałby ochoty tam zajrzeć, a testowanie polegało by głównie na odświeżaniu widoków w przeglądarce dla każdego z przypadków.</li>
<li>Teraz widok jest klasą, która &#8220;ma wiedzę&#8221; na temat tego jak się zachować w zależności od otrzymanych danych (<strong>w prostych przypadkach to już się dzieje</strong> &#8211; chociażby w akcjach &#8220;edit&#8221;). Wcześniej część tej wiedzy znajdowała się w kontrolerze.</li>
<li>Przyjemny side-effect wynikający z używania parametrów &#8220;named&#8221;: Teraz komponując link w widoku zamiast zastanawiać się czy formularz dodawania kosztów dla danej firmy jest pod /costs/add//[id_firmy]/ czy pod /costs/add///[id_firmy] &#8211; wiem, że jest pod /costs/add/entry_id:[id_firmy]</li>
</ol>
<p>Jest oczywiście brak w tym rozwiązaniu &#8211; dość sporo kodu wędruje do widoku (który, jak wspomniałem, trudno testować). Jednak pozostawienie architektury w pierwotnym stanie tej kwestii nie rozwiązuje. Na początku masz problem z </p>
<ul>
<li>dużą ilością kodu w kontrolerze</li>
<li>kodem kontrolera, który nie jest pokryty testami więc nie będzie refaktoryzowany</li>
<li>kodem w widoku, który nie jest pokryty testami</li>
</ul>
<p>Po zastosowanej zmianie zostanie Ci tylko ostatni punkt. Samodzielnie możesz zdecydować, czy to się opłaca.</p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2010/05/fat-model-skinny-controller-przyklad/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Dług techniczny &#8211; przykład z życia wzięty</title>
		<link>http://blog.grzegorzpawlik.com/2010/03/dlug-techniczny-przyklad-z-zycia-wziety/</link>
		<comments>http://blog.grzegorzpawlik.com/2010/03/dlug-techniczny-przyklad-z-zycia-wziety/#comments</comments>
		<pubDate>Sat, 20 Mar 2010 11:24:22 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[Agile]]></category>
		<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[dług techniczny]]></category>
		<category><![CDATA[element]]></category>
		<category><![CDATA[refactoring]]></category>
		<category><![CDATA[View]]></category>

		<guid isPermaLink="false">http://blog.grzegorzpawlik.com/?p=867</guid>
		<description><![CDATA[Można się spierać co jest długiem technicznym, a co nie jest. Chciałbym Wam pokazać przykład kodu, który świetnie nadaje się do zobrazowania problemu. Problem: Na jeden ze stron jest wyszukiwarka- formularz, w którym po wpisaniu danych w inputach i selectach &#8230; <a href="http://blog.grzegorzpawlik.com/2010/03/dlug-techniczny-przyklad-z-zycia-wziety/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Można się spierać co jest długiem technicznym, a co nie jest. Chciałbym Wam pokazać przykład kodu, który świetnie nadaje się do zobrazowania problemu.</p>
<p><strong>Problem:</strong><br />
Na jeden ze stron jest wyszukiwarka- formularz, w którym po wpisaniu danych w inputach i selectach przesyła te dane jako parametry &#8220;named&#8221;, np.:<br />
example.com/controller/action/field1:value1/field2:value2<br />
Potrzebny jest kawałek kodu javascript, który wygeneruje odpowiedni link i wywoła ten adres</p>
<p><strong>Rozwiązanie 1:</strong></p>
<pre name="code" class="javascript">
function submitform()
{
    form = document.getElementById('car_form');
    name = form.elements["CarName"].value;
    registration_number = form.elements["CarRegistrationNumber"].value;
    entry = document.getElementById('CarEntry').value
    driver = document.getElementById('CarDriver').value
    linkForm = '<?php echo $html->url('/cars/find/')?>';
    location = linkForm+'name:'+name+'/registration_number:'+registration_number+'/entry:'+entry+'/driver:'+driver;
    document.forms["car_form"].action = location;
}
</pre>
<p>Jest ono poprawne. Warto się zastanowić, czy został w tym przypadku zaciągnięty dług techniczny? Na pewno będą tacy,  którzy od razu będą wiedzieć co z tym kodem jest nie tak. Inni będą to czuli w kościach. Resztę pocieszę mówiąc, że ta umiejętność przychodzi z czasem pod warunkiem, że chcesz się uczyć jak pisać dobry kod.</p>
<p>Ten formularz wisi sobie gdzieś w jakimś panelu admina w listingu samochodów. Jednak taka wyszukiwarka jest potrzebna też przy listingu kierowców. Oczywiście kopiujesz rozwiązanie i poprawiasz je do nowych warunków. Nie ma w tym nic złego pod warunkiem, że robisz to z odpowiednim nastawieniem: &#8220;kopiuję kod, żeby móc zauważyć części wspólne i przeprowadzić refactoring&#8221; (czasem łatwiej mieć dwa dublujące się elementy i na ich podstawie tworzyć uniwersalny element/funkcje niż wymyślać ją od zera). </p>
<p>No i zabierając się za refactoring &#8211; w tym wypadku chciałbym zbudować wspólny element (dla tych co raczej siedzą w RoR &#8211; partial), który wywołam sobie tam, gdzie potrzebuję &#8211; zaczynam widzieć, że teraz oto przyjdzie mi spłacić ów mistyczny dług techniczny. Na czym on polega? Otóż w moim przykładzie funkcja budująca url jest strasznie &#8220;sztywna&#8221;. Działa w tym i tylko w tym przypadku. Nie ma najmniejszych znamion uniwersalności. Dlatego teraz, zanim zacznę myśleć o elemencie, muszę tą funkcję przebudować na bardziej uniwersalną. Co to znaczy?</p>
<p>Powinna być w stanie złapać wszystkie inputy i selecty, które są istotne przy budowaniu url&#8217;a i iterując po nich sprytnie go zbudować. Mogła by wyglądać na przykład tak:</p>
<p><strong>Rowziązanie 2:</strong></p>
<pre name="code" class="javascript">
function submitform(){
    var params = "";

    $("#car_form input[type!=submit][name!=_method], #car_form select").each(
    	    function(index,element) {
        	    params +=
        	    		  element.name.substr(
        	    				  element.name.lastIndexOf("[")
        	    		  ).slice(1, -1)+
        	    		  ":"+
        	    		  element.value+
        	    		  "/";
    	    }
    );
    window.location.href = '<?php echo $html->url('/cars/find/')?>' + params;
    return false;
}
</pre>
<p>Teraz inputy i selecty są zbierane bardziej automatycznie ("#car_form input[type!=submit][name!=_method], #car_form select"), a nazwy parametrów są brane z parametru name tych inputów i selectów. Dzięki temu powinna działać dla każdego formularza. Mniejsza o szczegóły.</p>
<p>Jednak rozwiązanie 1 nie było złe. Było wręcz idealne (bo proste i zrozumiałe) tak długo, jak było potrzebne w jednym miejscu. Rozwiązanie 2 mogło zająć przynajmniej 4 razy tyle czasu co pierwsze. Dlatego dopóki nie było powtórzeń kodu - było dobre. Dług techniczny nie istniał. Jednak pojawił się w momencie, kiedy zacząłem refaktoring kodu. </p>
<p>Jak to możliwe, że dług techniczny pojawił się w momencie, kiedy zacząłem go "spłacać"?<br />
Czy istniał cały czas i był ukryty, a ja go nagle odkryłem?</p>
<p>Odpowiem na to pytanie nie wprost: Dług techniczny jest tylko pojęciem umożliwiającym zrozumienie ludziom, kŧórzy nie są programistami, dlaczego nie wolno im zachęcać programistów do chodzenia na skróty. Uzmysławia też (mam nadzieję) niedoświadczonym programistom jakie są niebezpieczeństwa związane z chodzeniem na skróty.</p>
<p><del datetime="2010-03-20T10:48:45+00:00">Leniwy</del> <a href="/?p=29">Dobry</a>, doświadczony programista w ogóle nie musi zagłębiać się w szczegóły czy dług techniczny istnieje, czy nie. <strong>Po prostu tworzy dobry kod</strong>.</p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2010/03/dlug-techniczny-przyklad-z-zycia-wziety/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>How to merge edit() and add() methods in cakephp&#8230;</title>
		<link>http://blog.grzegorzpawlik.com/2010/03/how-to-merge-edit-and-add-methods-in-cakephp/</link>
		<comments>http://blog.grzegorzpawlik.com/2010/03/how-to-merge-edit-and-add-methods-in-cakephp/#comments</comments>
		<pubDate>Fri, 05 Mar 2010 05:00:35 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[Inne]]></category>
		<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[Controller]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[refactoring]]></category>
		<category><![CDATA[View]]></category>

		<guid isPermaLink="false">http://blog.grzegorzpawlik.com/?p=825</guid>
		<description><![CDATA[When You get a closer look at baked controllers You can get the idea that they&#8217;re breaking DRY principle. Can You tell which of two methods in baked controller are almost identical? If not &#8211; check out which two views &#8230; <a href="http://blog.grzegorzpawlik.com/2010/03/how-to-merge-edit-and-add-methods-in-cakephp/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>When You get a closer look at <a href="http://book.cakephp.org/view/113/Code-Generation-with-Bake">baked controllers</a> You can get the idea that they&#8217;re breaking <a href="http://en.wikipedia.org/wiki/DRY">DRY principle</a>.</p>
<p>Can You tell which of two methods in baked controller are almost identical? If not &#8211; check out which two views are even more similar. It&#8217;s not much of a riddle if You read title of this post again, though. That&#8217;s right.</p>
<p>Lets look at views first (in this example we have Thing model wit one field called &#8216;name&#8217;):</p>
<pre name="code" class="php">
// app/views/things/add.ctp
&lt;div class="things form"&gt;
&lt;?php echo $this-&gt;Form-&gt;create('Thing');?&gt;
   &lt;fieldset&gt;
      &lt;legend&gt;
         &lt;?php printf(__('Add %s', true), __('Thing', true)); ?&gt;
      &lt;/legend&gt;
   &lt;?php
      echo $this-&gt;Form-&gt;input('name');
   ?&gt;
      &lt;/fieldset&gt;
&lt;?php echo $this-&gt;Form-&gt;end(__('Submit', true));?&gt;
&lt;/div&gt;
&lt;div class="actions"&gt;
   &lt;h3&gt;&lt;?php __('Actions'); ?&gt;&lt;/h3&gt;
   &lt;ul&gt;
      &lt;li&gt;
         &lt;?php echo $this-&gt;Html-&gt;link(
            sprintf(
               __('List %s', true),
               __('Things', true)
            ),
            array('action' =&gt; 'index')
         );?&gt;
      &lt;/li&gt;
   &lt;/ul&gt;
&lt;/div&gt;
</pre>
<pre name="code" class="php">
// app/views/things/edit.ctp
&lt;div class="things form"&gt;
&lt;?php echo $this-&gt;Form-&gt;create('Thing');?&gt;
   &lt;fieldset&gt;
      &lt;legend&gt;
         &lt;?php printf(__('Edit %s', true), __('Thing', true)); ?&gt;
      &lt;/legend&gt;
   &lt;?php
      echo $this-&gt;Form-&gt;input('id');
      echo $this-&gt;Form-&gt;input('name');
   ?&gt;
   &lt;/fieldset&gt;
&lt;?php echo $this-&gt;Form-&gt;end(__('Submit', true));?&gt;
&lt;/div&gt;
&lt;div class="actions"&gt;
   &lt;h3&gt;&lt;?php __('Actions'); ?&gt;&lt;/h3&gt;
   &lt;ul&gt;
      &lt;li&gt;
         &lt;?php echo $this-&gt;Html-&gt;link(
            __('Delete', true),
            array('action' =&gt; 'delete', $this-&gt;Form-&gt;value('Thing.id')),
            null,
            sprintf(
               __('Are you sure you want to delete # %s?', true),
               $this-&gt;Form-&gt;value('Thing.id')
            )
         ); ?&gt;
      &lt;/li&gt;
      &lt;li&gt;
         &lt;?php echo $this-&gt;Html-&gt;link(
            sprintf(__('List %s', true), __('Things', true)),
            array('action' =&gt; 'index')
         );?&gt;
      &lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
</pre>
<p>They differ in 3 line (of 16). If there&#8217;s a change in model (ie. adding one field) &#8211; It&#8217;s needed to edit two files. That&#8217;s stupid.<br />
Lets take a look at methods:</p>
<pre name="code" class="php">
// app/controllers/things_controller.ctp
function add() {
   if (!empty($this->data)) {
      $this->Thing->create();
      if ($this->Thing->save($this->data)) {
         $this->Session->setFlash(__('The thing has been saved', true));
         $this->redirect(array('action' => 'index'));
      } else {
         $this->Session->setFlash(
            __('The thing could not be saved. Please, try again.', true)
         );
      }
   }
}

function edit($id = null) {
   if (!$id &#038;&#038; empty($this->data)) {
      $this->Session->setFlash(__('Invalid thing', true));
      $this->redirect(array('action' => 'index'));
   }
   if (!empty($this->data)) {
      if ($this->Thing->save($this->data)) {
         $this->Session->setFlash(__('The thing has been saved', true));
         $this->redirect(array('action' => 'index'));
      } else {
         $this->Session->setFlash(
            __('The thing could not be saved. Please, try again.', true)
         );
      }
   }
   if (empty($this->data)) {
      $this->data = $this->Thing->read(null, $id);
   }
}
</pre>
<p>It&#8217;s clear that edit() metdod encloses add() method body. There would be no much change if You just paste add() method into second if statement, wouldn&#8217;t it?</p>
<p>We&#8217;ll fix it with few tricks. We need to know how $this->Thing->create() exactly works. It forces adding of a new element into DB (because save() method works as &#8216;update&#8217; or &#8216;create&#8217; and it depends on current model state). But if You don&#8217;t pass any parameters to create() method the only thing it does is cleaning $this->data and $this->id (&#8216;this&#8217; means model Thing in current context).<br />
There&#8217;s no way that $this->Thing->data contains any junk while executed, so it&#8217;s safe to drop this line &#8211; it will still work (is it?! &#8211; <a href="#security">spoiler</a>).<br />
Now whole method body is identical with code block in edit method. Lets drop add() method and it&#8217;s view. Alter old links pointing at /things/add to /things/edit/ (without param).<br />
Lets try out how does it work right now?</p>
<p>Based on version &#8211; 1.2 will crush, 1.3 shows &#8220;Invalid thing&#8221; alert. I&#8217;ll focus on 1.3 version (as far as I remember changing method signature from edit($id) to edit($id=null) should get You back on track in 1.2).</p>
<p>From now on we can assume that when there&#8217;s no id in url &#8211; it means that someone want to add new Thing, so it&#8217;s ok to delete this if statement:</p>
<pre name="code" class="php">
// app/controllers/things_controller.php
		if (!$id &#038;&#038; empty($this->data)) {
			$this->Session->setFlash(__('Invalid thing', true));
			$this->redirect(array('action' => 'index'));
		}
</pre>
<p>Now we can add new Things through edit() form- everything works ok now.<br />
It&#8217;s good to polish this view, though. &#8220;edit&#8221; header look silly while adding new Thing&#8230; so delete button.</p>
<p>pimp up Your legend tag:</p>
<pre name="code" class="php">
// app/views/things/edit.ctp
 		&lt;legend&gt;
 		   &lt;?php printf(
 		      (!is_null($this-&gt;data["Thing"]["id"])?__('Edit %s', true): __('Add %s', true)),
 		      __('Thing', true));
 		   ?&gt;
 		 &lt;/legend&gt;
</pre>
<p>and surround first li element in if block:</p>
<pre name="code" class="php">
// app/views/things/edit.ctp
&lt;?php if(!is_null($this-&gt;data["Thing"]["id"])): ?&gt;
   &lt;li&gt;
      &lt;?php echo $this-&gt;Html-&gt;link(
         __('Delete', true),
         array('action' =&gt; 'delete', $this-&gt;Form-&gt;value('Thing.id')),
         null,
         sprintf(
            __('Are you sure you want to delete # %s?', true),
            $this-&gt;Form-&gt;value('Thing.id')
         )
      ); ?&gt;
   &lt;/li&gt;
&lt;?php endif; ?&gt;
</pre>
<p>That&#8217;s it.</p>
<p><strong id="security"><br />
Removing $this->Thing->create() not so secure?</strong><br />
I said that after deleting this line nothing bad will happen.<br />
It&#8217;s true, but only if You make all the changes explained in this post. If You just remove $this->Thing->create() You&#8217;ll get an security issue. Someone could send prepared post request to this address with field named &#8220;data[Thing][id]&#8221; and edit an element through add action.<br />
If you heavily depends on ACL authorisation, and one group of users can add Things, and other can edit them &#8211; member of the first group can raise his privileges by well prepared post request.<br />
I think it&#8217;s rather rare situation. If You are interested in fixing this &#8211; please comment this post.</p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2010/03/how-to-merge-edit-and-add-methods-in-cakephp/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Tutorial: dashboard web 2.0 dla leniwych&#8230;</title>
		<link>http://blog.grzegorzpawlik.com/2010/02/tutorial_dashboard_web_2-0_dla_leniwyc/</link>
		<comments>http://blog.grzegorzpawlik.com/2010/02/tutorial_dashboard_web_2-0_dla_leniwyc/#comments</comments>
		<pubDate>Wed, 24 Feb 2010 14:15:26 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[Agile]]></category>
		<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[jQuery]]></category>
		<category><![CDATA[tutorial]]></category>

		<guid isPermaLink="false">http://blog.grzegorzpawlik.com/?p=706</guid>
		<description><![CDATA[&#8230; czyli dobrych programistów ;) W moim pierwszym tutorialu napiszę jak wydajnie korzystać z tego co daje Ci cakePHP i jQuery. Aktualnie cake opiera się na współpracy z innym frameworkiem javascript &#8211; prototype. Ale już w wersji 1.3 core cake&#8217;a &#8230; <a href="http://blog.grzegorzpawlik.com/2010/02/tutorial_dashboard_web_2-0_dla_leniwyc/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p><!--pagetitle:start--><br />
&#8230; <a href="/?p=29">czyli dobrych programistów</a> ;)</p>
<p>W moim pierwszym tutorialu napiszę jak wydajnie korzystać z tego co daje Ci cakePHP i jQuery. Aktualnie cake opiera się na współpracy z innym frameworkiem javascript &#8211; prototype. Ale już w wersji 1.3 <a href="http://cakephp.lighthouseapp.com/projects/42648-cakephp-1x/milestones/57813-130">core cake&#8217;a ma współpracować z jquery</a>. Już nie mogę się doczekać. Ten tutorial oparłem na wersji 1.3.0-beta dlatego, że przy okazji jego pisania chciałem rzucić okiem na kolejną wersję tego świetnego frameworka php. Jednak nie będę korzystał z żadnych helperów jquery, zatem większość z tych rad powinna pasować do wersji 1.2 (w razie problemów &#8211; <a href="#tab-1">chętnie służę pomocą</a>).<br />
Jest to mój pierwszy tutorial, więc proszę o wskazówki, <a href="#tab-1">komentarze</a> i odrobinę wyrozumiałości.</p>
<p>Efekt jaki chcemy osiągnąć można obejrzeć jako <a href="http://cakephp.grzegorzpawlik.com/tutorial1/">demo</a> oraz <a href="http://cakephp.grzegorzpawlik.com/tutorial1/src.tar.gz">źródła</a> na świetnym hostingu, który gorąco wszystkim <a href="/?p=324">webdeveloperom</a> i ich <a href="/?p=413">klientom</a> polecam: <a href="http://meta.vipserv.org/cakephp/tutorial1/">vipserv.org</a>.</p>
<p>W tym tekście zakładam, ze znasz podstawy cakePHP i javascript. Czyli przynajmniej zrobiłeś sławny <a href="http://book.cakephp.org/view/219/Blog">blog tutorial </a>, wiesz jak mniej więcej używać narzędzia bake, oraz potrafisz przynajmniej zniknąć element na stronie i zmienić jego kolor przy pomocy czystego javascript&#8217;u.</p>
<h3>Dashboard na zakładkach</h3>
<p>Zacznijmy od przygotowania przykładowej aplikacji. Wypiekamy (cake bake) aplikację złożoną z dwóch modeli Thing i Item (każda tabela składa się z id i pola tekstowego name).</p>
<p>Następnie tworzymy dla niej zbiorczy widok (tzw. dashboard, metoda all())</p>
<pre name="code" class="php">
// /app/controllers/items_controller.php
class ItemsController extends AppController {
 //...
 function all() {}
}
</pre>
<pre name="code" class="php">
// /app/views/items/all.ctp
&lt;?php echo $this-&gt;requestAction(
            array(
               "controller"=&gt; "items",
               "action"=&gt; "index"),
            array("return")
      ); ?&gt;
&lt;?php echo $this-&gt;requestAction(
            array(
               "controller"=&gt; "things",
               "action"=&gt; "index"),
            array("return")
      ); ?&gt;
</pre>
<p>Na razie nie przejmujcie się kwestią requestAction i wydajności. Zazwyczaj efekty requestAction należy umieszczać w cache&#8217;owanych elementach, ale w naszym przypadku już niedługo zrezygnujemy z samego requestAction.</p>
<div id="attachment_709" class="wp-caption alignnone" style="width: 310px"><a href="http://blog.grzegorzpawlik.com/wp-content/uploads/2010/02/zbiorczy_widok1-e1266927750988.png" rel="lightbox[706]" title="zbiorczy_widok1"><img src="http://blog.grzegorzpawlik.com/wp-content/uploads/2010/02/zbiorczy_widok1-300x142.png" alt="widok zbiorczy - dashboard" title="zbiorczy_widok1" width="300" height="142" class="size-medium wp-image-709" /></a><p class="wp-caption-text">tak to wygląda teraz</p></div>
<p>Zróbmy z tego widoku zakładki modyfikując widok all.ctp w następujący sposób:</p>
<pre name="code" class="php">
&lt;script type="text/javascript"&gt;
   $(document).ready(function() {
      $("#tabs").tabs();
   });
&lt;/script&gt;

&lt;div id="tabs"&gt;
   &lt;ul&gt;
      &lt;li&gt;&lt;a href="#tabs-1"&gt;Items&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="#tabs-2"&gt;Things&lt;/a&gt;&lt;/li&gt;
   &lt;/ul&gt;
   &lt;div id="tabs-1"&gt;
      &lt;?php echo $this-&gt;requestAction(
                  array(
                     "controller"=&gt; "items",
                     "action"=&gt; "index"),
                  array("return")
            ); ?&gt;
   &lt;/div&gt;
   &lt;div id="tabs-2"&gt;
      &lt;?php echo $this-&gt;requestAction(
                  array(
                     "controller"=&gt; "things",
                     "action"=&gt; "index"),
                  array("return")
            ); ?&gt;
   &lt;/div&gt;
&lt;/div&gt;
</pre>
<p>Żeby wypieczone widoki były &#8220;otoczone&#8221; ramką z zakładki &#8211; trzeba w nich dodać taki fragment na samym końcu plików index.ctp:</p>
<pre name="code" class="html">
&lt;div style="float: none; clear: both;"&gt;&amp;nbsp;&lt;/div&gt;
</pre>
<div id="attachment_718" class="wp-caption alignnone" style="width: 310px"><a href="http://blog.grzegorzpawlik.com/wp-content/uploads/2010/02/zakładki_1-e1266927904106.png" rel="lightbox[706]" title="zakładki_1"><img src="http://blog.grzegorzpawlik.com/wp-content/uploads/2010/02/zakładki_1-300x91.png" alt="Widok zakładek" title="zakładki_1" width="300" height="91" class="size-medium wp-image-718" /></a><p class="wp-caption-text">tak wyglądają nasze zakładki</p></div>
<p>Dodajmy więcej elementów, żeby zobaczyć jak zachowuje się pagination&#8230;</p>
<pre name="code" class="sql">
insert into items(`name`) values ('Item 4'), ('Item 5'), ('Item 6'), ('Item 7')
</pre>
<p>i zmieńmy parametry stronicowania, żeby efekty stronicowania zobaczyć szybko ;)</p>
<pre name="code" class="php">
class ItemsController extends AppController {
//...
	var $paginate = array(
	  'limit'=> 5
	);
//...
</pre>
<p>Widzimy, że w naszym zbiorczym widoku link ciągle prowadzi do items/index. Wolelibyśmy, żeby stronicowanie odbyło się w zakładkach.</p>
<p>Można oczywiście próbować rozwiązania via php. Oprogramować obsługę parametrów w metodzie all(), zmienić cel linków stronicowania i sortowania w widokach. I ogólnie dużo napracować się nad tym, żeby nasz kod wyglądał źle ;). Już za chwilę <strong>moc javascript i w szczególności jQuery przyjdzie nam z pomocą</strong>. Zanim to nastąpi &#8211; zamieńmy nasze zakładki na Ajaxowe zmieniając widok all.ctp:</p>
<pre name="code" class="php">
// views/items/all.ctp
&lt;script type="text/javascript"&gt;
   $(document).ready(function() {
      $("#tabs").tabs();
   });
&lt;/script&gt;

&lt;div id="tabs"&gt;
   &lt;ul&gt;
      &lt;li&gt;
         &lt;?php echo $html-&gt;link(
                  "Items",
                  array(
                     "controller"=&gt; "items",
                     "action"=&gt; "index"
                  )
               );
         ?&gt;
      &lt;/li&gt;
      &lt;li&gt;
         &lt;?php echo $html-&gt;link(
                  "Things",
                  array(
                     "controller"=&gt; "things",
                     "action"=&gt; "index"
                  )
               );
         ?&gt;
      &lt;/li&gt;
   &lt;/ul&gt;
&lt;/div&gt;
</pre>
<p>&#8230;prościzna, prawda? Kwestię requestAction też już mamy z głowy. Jednak pojawił się problem &#8211; wnętrze zakładek pojawiło się w powtórzonym layoucie.<br />
Szybko naprawimy to w app_controller.php przy pomocy RequestHanlder:</p>
<pre name="code" class="php">
// /app/app_controller.php
var $components = array("RequestHandler");
function beforeFilter(){
    if($this->RequestHandler->isAjax()){
      $this->layout = "ajax";
    }
}
</pre>
<p>Wrócmy do kwestii stronicowania (i sortowania przy okazji). Chcemy, aby kolejne strony ładowały się w danej zakładce. Wykorzystajmy do tego potęgę jQuery.<br />
Dodajemy taki prosty skrypt </p>
<pre name="code" class="javascript">
// views/items/all.ctp
   $('#tabs a[href*=/sort:],#tabs a[href*=/page:]').live('click', function(){
       $('#tabs>div:visible').load($(this).attr('href'));
          return false;
   });
</pre>
<p>Jeśli interesuje Cię co się w tym fragmencie dzieje, zapraszam na <a href="/?p=591">do innego wpisu</a>. W dużym skrócie: łapiemy linki, które mają w adresie &#8220;sort:&#8221; i &#8220;page:&#8221; i zmuszamy je aby przeładowały zawartość zakładki, zamiast całej strony.</p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2010/02/tutorial_dashboard_web_2-0_dla_leniwyc/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>Zcalenie edit() i add() w kontrolerze</title>
		<link>http://blog.grzegorzpawlik.com/2010/02/zcalenie-edit-i-add-w-kontrolerze/</link>
		<comments>http://blog.grzegorzpawlik.com/2010/02/zcalenie-edit-i-add-w-kontrolerze/#comments</comments>
		<pubDate>Wed, 17 Feb 2010 14:00:20 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[Controller]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[refactoring]]></category>
		<category><![CDATA[View]]></category>

		<guid isPermaLink="false">http://blog.grzegorzpawlik.com/?p=662</guid>
		<description><![CDATA[Gdy przyjrzysz się bliżej kontrolerom stworzonym przy pomocy narzędzia &#8216;bake&#8217; (lub takim, jakie powstają po wykonaniu tutoriala) możesz stwierdzić, że łamią one koncepcję DRY. Jesteś w stanie powiedzieć, które z dwóch metod są niemal identyczne? Jeśli nie &#8211; sprawdź, które &#8230; <a href="http://blog.grzegorzpawlik.com/2010/02/zcalenie-edit-i-add-w-kontrolerze/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Gdy przyjrzysz się bliżej kontrolerom stworzonym przy pomocy narzędzia <a href="http://book.cakephp.org/view/113/Code-Generation-with-Bake">&#8216;bake&#8217;</a> (lub takim, jakie powstają po wykonaniu <a href="http://book.cakephp.org/view/219/Blog">tutoriala</a>) możesz stwierdzić, że łamią one koncepcję <a href="http://pl.wikipedia.org/wiki/DRY">DRY</a>. Jesteś w stanie powiedzieć, które z dwóch metod są niemal identyczne? Jeśli nie &#8211; sprawdź, które dwa widoki są niemal identyczne. Ok, może spaliłem swoją zagadkę, bo w tytule tego postu jest wyraźna sugestia o czym będę mówił. Zgadza się.</p>
<p>Najpierw rzućmy okiem na widoki (tu w przykładzie dla Modelu Thing, który ma jedno pole &#8211; name):</p>
<pre name="code" class="php">
// app/views/things/add.ctp
&lt;div class="things form"&gt;
&lt;?php echo $this-&gt;Form-&gt;create('Thing');?&gt;
   &lt;fieldset&gt;
      &lt;legend&gt;
         &lt;?php printf(__('Add %s', true), __('Thing', true)); ?&gt;
      &lt;/legend&gt;
   &lt;?php
      echo $this-&gt;Form-&gt;input('name');
   ?&gt;
      &lt;/fieldset&gt;
&lt;?php echo $this-&gt;Form-&gt;end(__('Submit', true));?&gt;
&lt;/div&gt;
&lt;div class="actions"&gt;
   &lt;h3&gt;&lt;?php __('Actions'); ?&gt;&lt;/h3&gt;
   &lt;ul&gt;
      &lt;li&gt;
         &lt;?php echo $this-&gt;Html-&gt;link(
            sprintf(
               __('List %s', true),
               __('Things', true)
            ),
            array('action' =&gt; 'index')
         );?&gt;
      &lt;/li&gt;
   &lt;/ul&gt;
&lt;/div&gt;
</pre>
<pre name="code" class="php">
// app/views/things/edit.ctp
&lt;div class="things form"&gt;
&lt;?php echo $this-&gt;Form-&gt;create('Thing');?&gt;
   &lt;fieldset&gt;
      &lt;legend&gt;
         &lt;?php printf(__('Edit %s', true), __('Thing', true)); ?&gt;
      &lt;/legend&gt;
   &lt;?php
      echo $this-&gt;Form-&gt;input('id');
      echo $this-&gt;Form-&gt;input('name');
   ?&gt;
   &lt;/fieldset&gt;
&lt;?php echo $this-&gt;Form-&gt;end(__('Submit', true));?&gt;
&lt;/div&gt;
&lt;div class="actions"&gt;
   &lt;h3&gt;&lt;?php __('Actions'); ?&gt;&lt;/h3&gt;
   &lt;ul&gt;
      &lt;li&gt;
         &lt;?php echo $this-&gt;Html-&gt;link(
            __('Delete', true),
            array('action' =&gt; 'delete', $this-&gt;Form-&gt;value('Thing.id')),
            null,
            sprintf(
               __('Are you sure you want to delete # %s?', true),
               $this-&gt;Form-&gt;value('Thing.id')
            )
         ); ?&gt;
      &lt;/li&gt;
      &lt;li&gt;
         &lt;?php echo $this-&gt;Html-&gt;link(
            sprintf(__('List %s', true), __('Things', true)),
            array('action' =&gt; 'index')
         );?&gt;
      &lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
</pre>
<p>Różnią się dokładnie w trzech linijkach (na 16 łącznie). Jeśli zmienisz coś w jednym (na przykład dodasz pole) to będziesz musiał poprawić formularz w dwóch plikach. Czysta głupota.<br />
Przyjrzyjmy się jeszcze metodom:</p>
<pre name="code" class="php">
// app/controllers/things_controller.ctp
function add() {
   if (!empty($this->data)) {
      $this->Thing->create();
      if ($this->Thing->save($this->data)) {
         $this->Session->setFlash(__('The thing has been saved', true));
         $this->redirect(array('action' => 'index'));
      } else {
         $this->Session->setFlash(
            __('The thing could not be saved. Please, try again.', true)
         );
      }
   }
}

function edit($id = null) {
   if (!$id &#038;&#038; empty($this->data)) {
      $this->Session->setFlash(__('Invalid thing', true));
      $this->redirect(array('action' => 'index'));
   }
   if (!empty($this->data)) {
      if ($this->Thing->save($this->data)) {
         $this->Session->setFlash(__('The thing has been saved', true));
         $this->redirect(array('action' => 'index'));
      } else {
         $this->Session->setFlash(
            __('The thing could not be saved. Please, try again.', true)
         );
      }
   }
   if (empty($this->data)) {
      $this->data = $this->Thing->read(null, $id);
   }
}
</pre>
<p>Praktycznie całe ciało metody add() można by wkleić w miejsce drugiego if&#8217;a w metodzie edit() i niewiele by się zmieniło, prawda?</p>
<p>Kilka tricków pozwoli nam naprawić to marnotrawstwo. W zasadzie najważniejsza kwestia jest następująca &#8211; jak działa metoda $this->Thing->create()? Wymusi dodanie nowego elementu (dlatego, że metoda save() działa jako create lub update w zależności od stanu modelu). Ale nie przekazując żadnych parametrów do tej metody tak naprawdę sprowadzi się do wyczyszczenia $this->data i $this->id (this oznacza w tym przypadku model). W przypadku metody add() nie ma możliwości, żeby jakieś śmieci były w $this->Thing->data, więc możemy spokojnie usunąć tą linijkę &#8211; dalej będzie działać tak samo (czy na pewno? &#8211; <a href="#bezpieczenstwo">spoiler</a>).<br />
Teraz całkowicie ciało metody add() mogłoby zawrzeć się w metodzie edit(). Dlatego trzeba ją usunąć (razem z jej widokiem). A wszystkie linki, które prowadziły do /things/add niech teraz prowadzą do /things/edit.<br />
Warto sprawdzić, co się teraz dzieje?</p>
<p>W zależności od wersji &#8211; 1.2 się wywali, 1.3 pokaże informacje &#8220;Invalid thing&#8221;. Teraz skupię się na wersji 1.3 &#8211; bo na niej sprawdzam na bieżąco efekty pisząc ten wpis. (dla 1.2 jeśli dobrze pamiętam wystarczy wstępnie zmienić sygnaturę metody edit($id) na edit($id=null), dalsze kroki będą analogiczne, choć kod w szczegółach może się różnić)</p>
<p>Od teraz możemy założyć, że jeśli w url&#8217;u nie ma podanego id &#8211; to znaczy, że chcemy dodać nowy element, zatem tego if&#8217;a możemy usunąć:</p>
<pre name="code" class="php">
// app/controllers/things_controller.php
		if (!$id &#038;&#038; empty($this->data)) {
			$this->Session->setFlash(__('Invalid thing', true));
			$this->redirect(array('action' => 'index'));
		}
</pre>
<p>Możemy spokojnie teraz dodawać nowe elementy przez formularz- wszystko działa.<br />
Warto jeszcze dopieścić widok &#8211; bez sensu jest nagłówek &#8220;edit&#8221; przy dodawaniu elementu, oraz przycisk &#8220;delete&#8221;,<br />
fragment z legend poprawiamy na taki (dodałem wcięcia dla przejrzystości):</p>
<pre name="code" class="php">
// app/views/things/edit.ctp
 		&lt;legend&gt;
 		   &lt;?php printf(
 		      (!is_null($this-&gt;data["Thing"]["id"])?__('Edit %s', true): __('Add %s', true)),
 		      __('Thing', true));
 		   ?&gt;
 		 &lt;/legend&gt;
</pre>
<p>a pierwszy element li otaczamy if&#8217;ami tak:</p>
<pre name="code" class="php">
// app/views/things/edit.ctp
&lt;?php if(!is_null($this-&gt;data["Thing"]["id"])): ?&gt;
   &lt;li&gt;
      &lt;?php echo $this-&gt;Html-&gt;link(
         __('Delete', true),
         array('action' =&gt; 'delete', $this-&gt;Form-&gt;value('Thing.id')),
         null,
         sprintf(
            __('Are you sure you want to delete # %s?', true),
            $this-&gt;Form-&gt;value('Thing.id')
         )
      ); ?&gt;
   &lt;/li&gt;
&lt;?php endif; ?&gt;
</pre>
<p>I tyle.</p>
<p><strong id="bezpieczenstwo">Usunięcie $this->Thing->create() nie takie bezpieczne?</strong><br />
Napisałem, że po usunięciu tego elementu nic się złego nie stanie. Oczywiście jest to prawdą pod warunkiem, że wykonasz wszystkie czynności opisane w tym poscie. Jeśli tylko usuniesz $this->Thing->create() narażasz się na niebezpieczeństwo, że ktoś wyśle spreparowanego post&#8217;a pod ten adres. Jeśli poda w nim pole o nazwie data[Thing][id] &#8211; będzie mógł zmienić dane dla elementu o danym id. Jeśli masz sytuację taką, że jedna grupa może dodawać elementy, a inna je edytować i polegasz tylko na acl&#8217;ach (dana grupa ma dostęp do edit(), a kolejna do add()) &#8211; to w ten sposób możesz stworzyć lukę. Jednak ta sytuacja jest dość rzadka i to jest raczej temat na inny wpis, więc jeśli interesuje Cię rowiązanie tego problemu &#8211; zachęcam do komentowania.</p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2010/02/zcalenie-edit-i-add-w-kontrolerze/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Skróć swoje listingi</title>
		<link>http://blog.grzegorzpawlik.com/2010/02/skroc-swoje-listingi/</link>
		<comments>http://blog.grzegorzpawlik.com/2010/02/skroc-swoje-listingi/#comments</comments>
		<pubDate>Tue, 02 Feb 2010 17:00:48 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[Inne]]></category>
		<category><![CDATA[Agile]]></category>
		<category><![CDATA[CakePHP 1.2]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[element]]></category>

		<guid isPermaLink="false">http://blog.grzegorzpawlik.com/?p=615</guid>
		<description><![CDATA[Pewnie w niemal każdym swoim widoku index.ctp masz fragment kodu podobny do tego: &#60;?php $i = 0; foreach ($tracks as $track): $class = null; if ($i++ % 2 == 0) { $class = ' class="altrow"'; } ?&#62; &#60;tr&#60;?php echo $class;?&#62;&#62; &#8230; <a href="http://blog.grzegorzpawlik.com/2010/02/skroc-swoje-listingi/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Pewnie w niemal każdym swoim widoku index.ctp masz fragment kodu podobny do tego:</p>
<pre name="code" class="php">
&lt;?php
$i = 0;
foreach ($tracks as $track):
	$class = null;
	if ($i++ % 2 == 0) {
		$class = ' class="altrow"';
	}
?&gt;
	&lt;tr&lt;?php echo $class;?&gt;&gt;
		&lt;td&gt;
			&lt;?php echo $track['Track']['id']; ?&gt;
		&lt;/td&gt;
		&lt;td&gt;
			&lt;?php echo $track['Track']['name']; ?&gt;
		&lt;/td&gt;
		&lt;td&gt;
			&lt;?php echo $track['Car']['name']; ?&gt;
		&lt;/td&gt;
		&lt;td&gt;
			&lt;?php echo $track['Track']['begin']; ?&gt;
		&lt;/td&gt;
		&lt;td&gt;
			&lt;?php echo $track['Track']['end']; ?&gt;
		&lt;/td&gt;
		&lt;td&gt;
			&lt;?php echo $track['Track']['length']; ?&gt;
		&lt;/td&gt;
		&lt;td class="actions"&gt;
			&lt;?php echo $html-&gt;link(
			   __('Edycja', true),
			   array(
			      'action' =&gt; 'edit',
			      $track['Track']['id'] ,
			      $track['Car']['id']
			   )
			  );
			?&gt;

		&lt;/td&gt;
	&lt;/tr&gt;
&lt;?php endforeach; ?&gt;
&lt;/table&gt;
</pre>
<p>Chce Ci się pisać ten kod kilkadziesiąt razy w jednym projekcie? Mnie też nie. Dlatego napisałem prosty element, który to trochę automatyzuje (nazwałem go &#8220;list&#8221;):</p>
<pre name="code" class="php">
// app/views/elements/list.ctp
&lt;?php
   $i = 0;
   foreach($elements as $key =&gt; $element):
   $class = null;
   if ($i++ % 2 == 0) {
      $class = ' class="altrow"';
   }
?&gt;
   &lt;tr &lt;?php echo $class?&gt;&gt;
      &lt;?php foreach($fields as $path): ?&gt;
         &lt;td&gt;
            &lt;?php echo Set::classicExtract($element, $path) ?&gt;
         &lt;/td&gt;
      &lt;?php endforeach; ?&gt;
         &lt;?php if(isset($links)): ?&gt;
            &lt;td&gt;
               &lt;?php foreach($links as $name =&gt; $url): ?&gt;
                  &lt;?php
                     if(!is_array($url["params"])) {
                        $url["params"] = array($url["params"]);
                     }
                     foreach($url["params"] as $key =&gt; $path){
                        $url[$key] = Set::classicExtract(
                           $element,
                           $path
                        );
                     }
                     unset($url["params"]);
                  ?&gt;
                  &lt;?php echo $html-&gt;link($name, $url)?&gt;
               &lt;?php endforeach; ?&gt;
            &lt;/td&gt;
         &lt;?php endif; ?&gt;
   &lt;/tr&gt;
&lt;?php endforeach; ?&gt;
</pre>
<p>Jego użycie jest dość proste. Dla przykładu wygenerujemy ten sam &#8220;output&#8221; dla pierwszego listingu z tego postu:</p>
<pre name="code" class="php">
echo $this->renderElement(
         "list",
         array(
            "elements" => $tracks,
            "fields" => array(
               "Track.name", "Car.name", "Track.begin",
               "Track.end", "Track.length"
            ),
            "links"=> array(
               "edycja" => array(
                  "controller"=> "tracks", "action"=> "edit",
                  "params" => array(
                     "Track.id", "Car.id"
                  )
               )
            )
         )
      )
</pre>
<p>Opiszę poszczególne parametry przekazywane do tego elementu:</p>
<ul>
<li>&#8220;elements&#8221; &#8211; kolekcja (w tym wypadku tablica) elementów zwrócona na przykład przez $this->Track->find(&#8220;all&#8221;)</li>
<li>&#8220;fields&#8221; &#8211; pola, które mają się pojawić w komórkach tabeli</li>
<li>&#8220;links&#8221; &#8211; tu definiujemy jakie linki mają się pojawić w ostatniej kolumnie (zazwyczaj tam znajdują się &#8220;opcje&#8221;). Indeksy kolejnych elementów to nazwy linków (tutaj &#8220;edycja&#8221;).
<ul>
<li>&#8220;controller&#8221; i &#8220;action&#8221; są identycznie używane jak w metodzie <a href="http://api12.cakephp.org/class/html-helper#method-HtmlHelperlink">HtmlHelper::link()</a></li>
<li>&#8220;params&#8221; jest dość ciekawy. Jeśli chcemy w wygenerowanym linku mieć jeden parametr (najczęściej id), robimy to tak:
<pre name="code" class="php">
            "edycja" => array(
               "controller"=> "cars", "action"=> "edit", "params"=>"Car.id"
            ),
</pre>
<p>gdy parametrów ma być więcej &#8211; ścieżki do nich przekazujemy w tablicy (tak jak w przykładzie):</p>
<pre name="code" class="php">
                "edycja" => array(
                   "controller"=> "tracks", "action"=> "edit",
                   "params" => array(
                      "Track.id", "Car.id"
                   )
                )
</pre>
<p>Można także używać parametrów typu <a href="http://book.cakephp.org/pl/view/541/Named-parameters">&#8220;named&#8221;</a>, wystarczy podać dodatkowo indeksy:</p>
<pre name="code" class="php">
                "edycja" => array(
                   "controller"=> "tracks", "action"=> "edit",
                   "params" => array(
                     "parametr_1"=> "Track.id", "parametr_2"=> "Car.id"
                   )
                )
</pre>
</li>
</ul>
</ul>
<p>Zachęcam do korzystania z tego elementu i zgłaszania wszelkich niedociągnięć :)</p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2010/02/skroc-swoje-listingi/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Dobre praktyki programowania w CakePHP #4</title>
		<link>http://blog.grzegorzpawlik.com/2009/10/dobre-praktyki-programowania-w-cakephp-4/</link>
		<comments>http://blog.grzegorzpawlik.com/2009/10/dobre-praktyki-programowania-w-cakephp-4/#comments</comments>
		<pubDate>Mon, 12 Oct 2009 20:12:34 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[beforeFilter]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[mvc]]></category>

		<guid isPermaLink="false">http://blog.grzegorzpawlik.com/?p=365</guid>
		<description><![CDATA[O podobnych kwestiach pisałem już przy okazji wpisu #2 z tej serii. Ale warto jeszcze raz przypomnieć o tym, że tak jak zasady projektowania obiektowego tak należy dokładnie zrozumieć co oznacza podział aplikacji na warstwy MVC. W tym wpisie skupię &#8230; <a href="http://blog.grzegorzpawlik.com/2009/10/dobre-praktyki-programowania-w-cakephp-4/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>O podobnych kwestiach pisałem już przy okazji wpisu <a href="/?p=53">#2 z tej serii</a>. Ale warto jeszcze raz przypomnieć o tym, że tak jak <a href="/?p=279">zasady projektowania obiektowego</a> tak należy dokładnie zrozumieć co oznacza podział aplikacji na warstwy MVC.</p>
<p>W tym wpisie skupię się na <strong>warstwie modelu</strong>.<br />
Jest ona czasem nazywana warstwą biznesową aplikacji- i nie jest to przypadek. Dzieje się tak dlatego, że w niej zawarte są (albo powinny być) reguły biznesowe, które dany system realizuje. Przejdę jak najszybciej do przykładu, który to obrazuje.</p>
<p>Wyobraź sobie system zarządzania firmą wypożyczającą samochody. Cake wypiekł Ci CRUD dla modelu Cars i Costs (koszty generowane przez te samochody). Teraz klient informuje Cię o pewnej zasadzie, która brzmi:<br />
&#8220;Każdy koszt można powiązać z dowolną ilością samochodów. Koszt może nie być powiązany z żadnym samochodem (koszt ogólny). Dane powiązanie koszt-samochód jest opatrzone konkretną kwotą (kwota powiązania). Jeśli koszt jest powiązany z jednym lub więcej samochodami, to suma kwoty ich powiązań musi się równać wartości kosztu. Czyli koszt musi być całkowicie rozdzielony na samochody, lub wcale.<br />
Na przykładzie: mogę dodać koszt &#8216;naprawa po wypadku&#8217;, który powiążę z jednym samochodem na pełną kwotę. Mogę dodać koszt &#8216;ubezpieczenie za rok 2008&#8242;, które rozdzielę po równo między wszystkimi samochodami i mogę dodać &#8216;spinacze do biura&#8217;, którego nie powiąże z żadnym samochodem&#8221;.<br />
To co właśnie usłyszałeś to reguła biznesowa, którą Twoja aplikacja musi obsłużyć.</p>
<p>Od razu oczywiście zabierasz się ochoczo za zdefiniowanie relacji wiele do wielu (HABTM) w modelach Cost i Car. Jeśli uważasz, że to wszystko i modele już będą jak zwykle służyć do wykonywania selectów na bazie- to sygnał, że możesz jeszcze nie rozumieć idei MVC, więc czytaj dalej.</p>
<p>Zabierasz się za implementowanie funkcjonalności. Bez większego zagłębiania się w szczegóły zbudujesz widok, w którym wybierzesz dla danego kosztu ileś samochodów z listy, wypełnisz ich kwoty i po kliknięciu zapisz poleci wszystko do kontrolera, a w nim będziesz miał 4 (o cztery za dużo) foreachów, którymi będziesz sprawdzał, czy suma się zgadza, a jeśli tak &#8211; pozwolisz na zapisz, bla, bla, bla. Jesteś zadowolony.</p>
<p>Jednak za miesiąc przychodzi klient i mówi: &#8220;fajne jest to przypisywanie kosztów do samochodów, ale ja chciałbym, żeby móc do kosztu przypisać samochody tylko na część kwoty&#8221;.<br />
Jako porządny podwykonawca zgadzasz się, dajesz nura w kontroler i rozdmuchujesz go o pięć nowych ifów i trzy foreache, żeby obsłużyć wszystkie niuanse, które właśnie się pojawiły.</p>
<p>Ale licho&#8230; ekhm, klient nie śpi: &#8220;Skoro można przypisywać samochody do kosztów nie w całości &#8211; to chciałbym móc w widoku samochodu możliwość wybrania kosztów i je do niego przypisać&#8221;.<br />
I co teraz robisz?</p>
<p>Jeśli zabierasz się za przeniesienie mechanizmów z kontrolera Costs do modelu Cost &#8211; może nie pójdziesz jeszcze do piekła. Jeśli zapaliła Ci się czerwona lampka, ale nie wiesz o co chodzi &#8211; czytaj dalej. Jeśli zaczynasz kopiować wspomniane mechanizmy do kontrolera Cars &#8211; jesteś w ciemnej du&#8230; hmm piwnicy. Dlaczego?</p>
<p>To, że łamiesz zasadę MVC to już powinieneś czuć przez skórę. Możesz jeszcze nie wiedzieć czym to grozi. To, że łamiesz zasady projektowania obiektowego &#8211; mogłeś nie zauważyć. Ale, że masz w dwóch różnych miejscach (CostsController i CarsController) mechanizm robiący to samo i Ci to nie przeszkadza &#8211; za to właśnie trafisz do piekła.</p>
<p>Powtórzenie, które przed chwilą opisałem ma dwie poważne konsekwencje:<br />
1. Mechanizmy dbające o spójność bazy danych masz rozproszone w wielu klasach. Dlatego zmiana reguły biznesowej pociąga za sobą konieczność zmiany wielu dublujących się wierszy kodu. Łatwo wtedy o pomyłkę.</p>
<p>2. Model nie dba samodzielnie o spójność danych. Pisząc w ten sposób aplikację zakładasz, że każdy programista w każdym momencie rozwoju aplikacji (czyli nawet za dwa lata) będzie dokładnie pamiętał tą (i wszystkie inne) reguły biznesowe. Dodając kolejny element, znów będzie musiał samodzielnie zadbać o każdy aspekt spójności danych. Łatwo wtedy o pomyłkę.</p>
<p>Dlatego o spójność danych <strong>musi dbać</strong> warstwa modelu. Bo ona dba o sama siebie. Jeśli odpowiednio zaprojektujesz model Cost, przy pomocy callback&#8217;ów beforeValidate, before i afterSave zawrzesz w modelu zapewnienie spójności, wszystko co pozostanie w przyszłości do zrobienia to przygotowanie odpowiedniej tablicy w kontrolerze i wywołanie $this-&gt;Cost-&gt;save(&#8230;).</p>
<p>Dlatego czwartą zasadę formułuję następująco:<br />
<strong><span style="font-size: large;">mechanizmy dbające o spójność bazy danych umieszczaj w warstwie modelu</span></strong></p>
<p>ps. Witam wszystkich po przydługiej przerwie wakacyjno-inietylko ;)</p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2009/10/dobre-praktyki-programowania-w-cakephp-4/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Dobre praktyki programowania w CakePHP #3</title>
		<link>http://blog.grzegorzpawlik.com/2009/06/dobre-praktyki-programowania-w-cakephp-3/</link>
		<comments>http://blog.grzegorzpawlik.com/2009/06/dobre-praktyki-programowania-w-cakephp-3/#comments</comments>
		<pubDate>Sun, 21 Jun 2009 17:45:34 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[DRY]]></category>

		<guid isPermaLink="false">http://blog.grzegorzpawlik.com/?p=279</guid>
		<description><![CDATA[Tym razem krótka notka&#8230; Zauważyłem, że czasem gdy stosujemy wzorzec MVC, zapominamy* że istnieje coś takiego jak projektowanie i programowanie zorientowane obiektowo. (*)zakładając, że wcześniej wiedzieliśmy, że coś takiego istnieje &#8211; to nie jest takie oczywiste. Chodzi mi o to, &#8230; <a href="http://blog.grzegorzpawlik.com/2009/06/dobre-praktyki-programowania-w-cakephp-3/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Tym razem krótka notka&#8230; </p>
<p>Zauważyłem, że czasem gdy stosujemy wzorzec MVC, zapominamy* że istnieje coś takiego jak projektowanie i programowanie zorientowane obiektowo.<br />
(*)zakładając, że wcześniej wiedzieliśmy, że coś takiego istnieje &#8211; to nie jest takie oczywiste.</p>
<p>Chodzi mi o to, że jak piszesz class, private i extends to jeszcze za mało, żeby powiedzieć iż Twój kod jest obiektowy. Na razie możesz powiedzieć, że masz klasy i metody. Ale dalej możesz mieć bajzel jak bez nich.</p>
<p>Dlatego powstają takie niespodzianki jak metoda getAllComments($postId) w kontrolerze Posts, bo akurat wyświetlając dany post potrzebujesz wyświetlić komentarze. Chwała i tak się należy, bo ktoś umieścił ten fragment w osobnej metodzie. Jednak to jeszcze za mało, żeby nie musieć się kodu wstydzić.</p>
<p>Jeśli przypomnisz sobie podstawy programowania obiektowego ze studiów, czy książki o C++ może taki przykład wyda Ci się znajomy:<br />
- Auto jest obiektem: posiada atrybuty szybkość_maksymalna, ilość_pasażerów oraz metody jedź(), ruszaj(), zatrzymaj_się(), włącz_klimę() ;)<br />
To jest przykład w którym wyjaśnia się czym są obiekty, jak ich używać. Czasem brakuje jednak informacji &#8211; prostej odpowiedzi na pytanie:<br />
&#8220;Dlaczego klasa Auto nie posiada metody wypij_kawę_w_samochodzie()?&#8221;. To już zagadnienie z OODesign.</p>
<p>W przypadku samochodu jest to oczywiste, ale łatwo zapomnieć o tym, kiedy nasze klasy są mniej &#8220;dotykalne&#8221; i sami je projektujemy, a nie &#8220;mapujemy&#8221; ze świata rzeczywistego. Dlatego skoro wiemy, że metoda wypij_kawę_w_samochodzie() powinna należeć do klasy Człowiek &#8211; to dlaczego każemy klasie Posts znajdować komentarze?</p>
<p>Dochodzę już do sedna: przy projektowaniu aplikacji w CakePHP (i w każdym innym frameworku opartym na MVC lub nie) nie możesz olewać zasad projektowania obiektowego. Nie możesz ignorować problemu przynależenia metod do odpowiednich klas. Dlaczego?</p>
<p>Przykład:<br />
Gdy w kilka osób rozwijacie aplikację i nie mieliście 10tysięcy na wynajęcie architekta, który zaprojektowałby wszystkie klasy i ich metody, to prawdopodobnie rozwijacie architekturę stopniowo. W przypływie twórczości jeden z kolegów umieścił metodę getAllComments() w kontrolerze Posts. Następnie Ty na stronie główniej musisz umieścić ostatnie komentarz. Rzucasz okiem na klasę Comments i widzisz, że brakuje tam metody getAll() (Comments w domyśle) więc ją implementujesz.<br />
W wyniku łamana jest reguła <a href="/tag/dry/">DRY</a>, a co za tym idzie powstaje redundancja w kodzie. Zmniejsza się prostota projektu i rośnie <a title="Tutaj dowiesz się czym jest dług techniczny" href="http://97rzeczy.devblogi.pl/artykuly/1/postepuj-rozwaznie">dług techniczny</a>. </p>
<p>Może się wydawać, że to błahy przykład i to jest dobre wrażenie. Jednak jeśli nie myślisz o tym problemie takich kwiatków w projekcie prędzej czy później będziesz miał całą łączkę. Po jakimś czasie łączka zarasta tak bardzo, że nie sposób tam wejść z maczetą, więc zarzucasz projekt, bo nie nadaje się on już do konserwacji.</p>
<p>Zatem zasada numer 3:<br />
<strong><span style="font-size:large;">Poznaj zasady projektowania obiektowego, i nie wypinaj się na nie podczas programowania</span></strong></p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2009/06/dobre-praktyki-programowania-w-cakephp-3/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Zarządzanie wersjami STRUKTURY bazy danych w cakePHP 1.2</title>
		<link>http://blog.grzegorzpawlik.com/2009/03/zarzadzanie-wersjami-struktury-bazy-danych-w-cakephp-12/</link>
		<comments>http://blog.grzegorzpawlik.com/2009/03/zarzadzanie-wersjami-struktury-bazy-danych-w-cakephp-12/#comments</comments>
		<pubDate>Wed, 25 Mar 2009 14:39:00 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[CakePHP 1.2]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[zarządzanie projektem]]></category>

		<guid isPermaLink="false">http://meta.vipserv.org/blog.grzegorzpawlik.com/?p=62</guid>
		<description><![CDATA[W poprzednich postach (m.in. zarządzanie wersjami oprogramowania) udało mi się nakreślić problem przy zarządzaniu oprogramowaniem pojawiający się na styku kod-baza danych. Nawet mogę powiedzieć, że mały sukces na tym polu odnotowałem przy pomocy&#160;ImageBehavior, jednak jeśli chodzi o strukturę &#8211; ciągle &#8230; <a href="http://blog.grzegorzpawlik.com/2009/03/zarzadzanie-wersjami-struktury-bazy-danych-w-cakephp-12/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>W poprzednich postach (m.in. <a href="http://webbricks.blogspot.com/2008/12/zarzdzanie-wersjami-oprogramowania.html">zarządzanie wersjami oprogramowania</a>) udało mi się nakreślić problem przy zarządzaniu oprogramowaniem pojawiający się na styku kod-baza danych. Nawet mogę powiedzieć, że mały sukces na tym polu odnotowałem przy pomocy&nbsp;<a href="http://webbricks.blogspot.com/2009/03/imagebehavior-uploaduj-pliki-prosto-do.html">ImageBehavior</a>, jednak jeśli chodzi o strukturę &#8211; ciągle zmagałem się do tej pory z przeciwnościami.</p>
<p>Jednak okazuje się, że cake w nowym wydaniu wychodzi nam na przeciw razem z klasą Schema, oraz z narzędziem konsolowym ./cake schema &#8230; po krótce opowiem o co chodzi.</p>
<p>Zabawę z tym narzędziem najlepiej zacząć mając już jakiś zalążek aplikacji (tabele + modele). Jeśli sprawiamy ten podstawowy warunek możemy wpisać w konsoli ./cake schema generate &#8230; ot tak, dla jaj.</p>
<p>Następnie możemy się w katalogu app/config/sql/ namierzyć plik schema.php. To właśnie artefakt wygenerowany przez nas przed sekundą. Można w celach samorozwojowych zajrzeć do środka&#8230;</p>
<p>Jednak ciekawe rzeczy dzieją się, kiedy ponownie wywołamy to samo polecenie: otóż cake rezolutnie zauważy, że plik schema.php już istnieje i zapyta nas co dalej. Polecam wybór opcji [S]napshot i ponowny rzut oka do wspomnianego wyżej katalogu. Co widzimy? Dokładnie! Nowy plik o nazwie schema_2.php :D Zachęcam do zapoznania się z helpem (./cake schema help).</p>
<p>Wystarczy, że teraz przekonam zespół, aby w sytuacji, gdy nastąpiły zmiany w bazie, przed commitem wywołali to polecenie. Jest jeden problem, którego ewentualnie można się spodziewać &#8211; sporadycznych konfliktów. To znaczy sytuacji, w której dwóch programistów:</p>
<ol>
<li> ściąga repozytorium,&nbsp;</li>
<li>dokonuje (nawet różnych) zmian w bazie,&nbsp;</li>
<li>zatwierdza dane:&nbsp;</li>
<ol>
<li>schema generate,&nbsp;</li>
<li>svn add schema_X.php,&nbsp;</li>
<li>svn commit</li>
</ol>
</ol>
<p>Problem w tym, że w takiej sytuacji w punkcie 3.3 jeden z nich dostanie informację</p>
<blockquote><p>Nie mogę dodać schema_X.php do repozytorium, gdyż takowy&nbsp; już w repozytorium istnieje.<br />
Z poważaniem Twój<br />
SVN</p></blockquote>
<p>Nie jest to jakaś wielka tragedia, jak przy każdym konflikcie trzeba będzie go rozwiązać (w tym wypadku przy spotkaniu tych dwóch programistów). Jednak myślę, że takie sytuacje można by zlikwidować wywołując tą sekwencję w jednym ciągu (nie np. commit po dwóch godzinach od schema generate), może nawet napisać prosty skrypt, który załatwi to za nas (taki svncommitwithcakeschemagenerate.sh ;))</p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2009/03/zarzadzanie-wersjami-struktury-bazy-danych-w-cakephp-12/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>ImageBehavior &#8211; uploaduj pliki prosto do bazy</title>
		<link>http://blog.grzegorzpawlik.com/2009/03/imagebehavior-uploaduj-pliki-prosto-do-bazy/</link>
		<comments>http://blog.grzegorzpawlik.com/2009/03/imagebehavior-uploaduj-pliki-prosto-do-bazy/#comments</comments>
		<pubDate>Tue, 03 Mar 2009 13:01:00 +0000</pubDate>
		<dc:creator>Greg</dc:creator>
				<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[behavior]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[RAD]]></category>

		<guid isPermaLink="false">http://meta.vipserv.org/blog.grzegorzpawlik.com/?p=56</guid>
		<description><![CDATA[Jakiś czas temu napisałem o pomyśle cake&#8217;owego Behavior (http://webbricks.blogspot.com/2009/02/pliki-w-formie-binarnej-w-bazie.html). Poniżej prezentuję pierwsze podejście do problemu: /** * ImageBehavior - take best from database blobs adn file image storage * requires ’content’ field that is a blob (mediumblob or longblob), and &#8230; <a href="http://blog.grzegorzpawlik.com/2009/03/imagebehavior-uploaduj-pliki-prosto-do-bazy/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Jakiś czas temu napisałem o pomyśle cake&#8217;owego Behavior (<a href="http://webbricks.blogspot.com/2009/02/pliki-w-formie-binarnej-w-bazie.html">http://webbricks.blogspot.com/2009/02/pliki-w-formie-binarnej-w-bazie.html</a>). Poniżej prezentuję pierwsze podejście do problemu:</p>
<p><code lang="php"><br />
/**<br />
 * ImageBehavior - take best from database blobs adn file image storage</p>
<p> * requires ’content’ field that is a blob (mediumblob or longblob), and<br />
 * ’ext’ varchar(10) field  and</p>
<p> * ’modified’ datetime field<br />
 * @author Grzegorz Pawlik<br />
 * @version 1.0<br />
 */<br />
class ImageBehavior extends ModelBehavior {</p>
<p>   /**<br />
    * directory in which cached files will be stored<br />
    *<br />
    * @var string<br />
    */<br />
   var $cacheSubdir = ‘filecache’;</p>
<p>   /**<br />
    * if set to false - never check if cached file is present (nor actual)<br />
    *</p>
<p>    * @var bool<br />
    */<br />
   var $usecache = true;</p>
<p>   function setup(&#038;$Model) {<br />
      // no setup at this time</p>
<p>   }</p>
<p>   /**<br />
    * Insert proper blob when standard data after upload is present<br />
    *<br />
    * @param object $Model</p>
<p>    * @return bool true<br />
    */<br />
   function beforeSave(&#038;$Model) {</p>
<p>      if(isset($Model->data[$Model->name]['file']['tmp_name']) &#038;&#038; is_uploaded_file($Model->data[$Model->name]['file']['tmp_name'])) {</p>
<p>      // podnieś wyżej parametry<br />
      $Model->data[$Model->name] = array_merge($Model->data[$Model->name],  $Model->data[$Model->name]['file']);</p>
<p>      // przygotuj blob<br />
      $this->_prepareBlob($Model);</p>
<p>      $this->_getExt($Model);<br />
      }</p>
<p>      return true;<br />
   }</p>
<p>   /**<br />
    * prepares blob contents<br />
    *<br />
    * @param object $Model</p>
<p>    */<br />
   function _prepareBlob(&#038;$Model) {<br />
      App::import(‘Core’, ‘File’);<br />
      $file = new File($Model->data['Medium']['tmp_name'], false);</p>
<p>      $content = $this->addSlashes( $file->read() );<br />
      $Model->data[$Model->name]['content'] = $content;</p>
<p>   }</p>
<p>   /**<br />
    * Get uploaded file extension<br />
    *<br />
    * @param object $Model<br />
    */<br />
   function _getExt(&#038;$Model) {</p>
<p>      $file = explode(‘.’, $Model->data['Medium']['name']);<br />
      $ext = array_pop($file);</p>
<p>      $Model->data[$Model->name]['ext'] = $ext;<br />
   }</p>
<p>   /**<br />
    * replace blob contents with file path</p>
<p>    * After reading database checks if cached file is present. If not creates it (from blob contents) and</p>
<p>    * returns a ’file’ field with path relative to /app/webroot/img<br />
    *<br />
    *<br />
    * @param object $model</p>
<p>    * @param array $results<br />
    * @param unknown_type $primary<br />
    * @return unknown<br />
    */<br />
   function afterFind(&#038;$model, $results, $primary) {</p>
<p>      foreach($results as $key => $val) {</p>
<p>         $relpath = $this->cacheSubdir . DS .</p>
<p>                 $val[$model->name]['id'] . ‘_’ . $model->name . ‘_’ .</p>
<p>                 $val[$model->name]['modified'] . ‘.’ . $val[$model->name]['ext'];</p>
<p>         $relpath = str_replace( array(‘ ’, ‘:’) , ‘_’, $relpath);</p>
<p>         $fullpath = IMAGES . $relpath;</p>
<p>         if(!file_exists($fullpath) || !$this->usecache ) {<br />
            file_put_contents($fullpath, $this->stripSlashes($results[$key][$model->name]['content']));</p>
<p>         }</p>
<p>         $results[$key][$model->name]['file'] = $relpath;<br />
         // remove blob from results (its messy when You want to output results in debug)</p>
<p>         unset($results[$key][$model->name]['content']);<br />
      }<br />
      return $results;<br />
   }</p>
<p>   /**<br />
    * add slashes (just wrapper)<br />
    *<br />
    * @param string $string<br />
    * @return string with slashes<br />
    */</p>
<p>   function addSlashes($string) {<br />
      return addslashes($string);<br />
   }</p>
<p>   /**<br />
    * strip slashes (just wrapper)</p>
<p>    *<br />
    * @param string $string<br />
    * @return string without slashes<br />
    */<br />
   function stripSlashes($string) {</p>
<p>      return stripslashes($string);<br />
   }<br />
}<br />
</code></p>
<p>Zasada działania jest dość prosta. Wyjaśnię ją na przykładzie.<br />
Tabela media:<br />
<code lang="mysql"><br />
CREATE TABLE IF NOT EXISTS `media` (<br />
`id` int(11) NOT NULL auto_increment,<br />
`name` varchar(50) NOT NULL,</p>
<p>`ext` varchar(10) NOT NULL,<br />
`content` longblob NOT NULL,<br />
`size` int(11) NOT NULL,<br />
`created` datetime NOT NULL,<br />
`modified` datetime NOT NULL,</p>
<p>`type` varchar(20) NOT NULL,<br />
PRIMARY KEY  (`id`)<br />
) ENGINE=MyISAM;<br />
</code><br />
Model:<br />
<code lang="php"><br />
class Medium extends AppModel {</p>
<p>var $name = ‘Medium’;<br />
var $actsAs = array(‘Image’);</p>
<p>}<br />
</code></p>
<p>Kontroler:<br />
<code lang="php"><br />
class MediaController extends AppController {</p>
<p>var $name = ‘Media’;<br />
var $helpers = array(‘Html’, ‘Form’);<br />
function index() {</p>
<p>$this->set(‘media’, $this->Medium->findAll());<br />
}</p>
<p>function add() {<br />
if(!empty($this->data)) {<br />
$this->Medium->save($this->data);</p>
<p>}<br />
}</p>
<p>}<br />
</code></p>
<p>Przy uploadzie ImageBehavior oczekuje, że plik będzie przekazany w poly ModelName.file (tutaj Media.file).</p>
<p>add.ctp:<br />
<code lang="php"><br />
<?php<br />
echo $form->create(<br />
array(‘url’ => array(<br />
‘controller’ => ‘media’,</p>
<p>‘action’ => ‘add’<br />
),<br />
‘enctype’ => ‘multipart/form-data’<br />
)<br />
);<br />
?></p>
<p><?php echo $form->file(‘Medium.file’); ?><br />
<?php echo $form->end(’submit’); ?><br />
</code></p>
<p>Przy odczycie dzieje się to co mnie najbardziej interesowało. Zamiast dostać zawartość (BLOB) pliku, dostajemy w polu file ścieżkę (relatywną do app/webroot/img). Domyślne ustawienia wymagają, żeby był tam katalog filecache (z możliwością zapisu). Przy operacji read behavior sprawdzi, czy istnieje aktualy plik w filecache, i jesli nie &#8211; utworzy go.</p>
<p>index.ctp:<br />
<code lang="php"><br />
<?php foreach($media as $medium): ?><br />
<?php echo $html->image($medium['Medium']['file']); ?><br />
</code></p>
<p>To rozwiązanie ma przynajmniej dwa zauważalne braki:</p>
<ol>
<li>Gdy dodamy taki plik do treści np. postu, po jego updacie &#8211; nie będą widoczne zmiany</li>
<li>Dobrze byłoby, gdyby przy operacji read nie zwracał zawartości BLOB (ważne, gdy baza jest gdzieś dalej), ale odpytywał tylko wtedy, gdy jest potrzebne zaktualizowanie zawartości pliku w filecache.</li>
</ol>
<p>To rzeczy do zrobienia w kolejnym podejściu ;)</p>
<hr/>
Aktualizacja: po umieszczeniu tego pomysłu na bakery niejaki Travis Rowland wrzucił ciekawe poprawki. Nie miałem okazji ich wypróbować, ale dla zainteresowanych: <a href="http://bakery.cakephp.org/articles/view/imagebehavior-best-from-database-blobs-and-file-storage">http://bakery.cakephp.org/articles/view/imagebehavior-best-from-database-blobs-and-file-storage</a></p>
<!-- PHP 5.x -->]]></content:encoded>
			<wfw:commentRss>http://blog.grzegorzpawlik.com/2009/03/imagebehavior-uploaduj-pliki-prosto-do-bazy/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

