Yuriy Babenko - drupal planet http://yuriybabenko.com/blog/tag/drupal-planet en devtools part 2 - multi-step forms in dialog modal http://yuriybabenko.com/blog/dialog-part-2-multi-step-forms-dialog-modal <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p>I recently <a href="http://yuriybabenko.com/blog/introducting-dev-tools" title="DevTools module">introduced the DevTools</a> module, and today I'm going to show you one of my favourite components, dialog, which builds upon the <a href="https://drupal.org/project/dialog" title="Dialog module">Dialog module</a> (fear not the 7.x-dev version, it's perfectly stable) and allows you to present single- or multi-state forms in a modal.</p> <p>First and foremost, a small disclaimer: the dialog component was built for use with custom forms, and may not have expected results with forms from core modules.</p> <h3>The plan</h3> <p>In the example module we're about to write, we're going to create a page with a link that will open a custom form in a dialog, proceed through three form states, and upon successful submission of the last state, update the original link with new text.</p> <h3>Getting started</h3> <p>The first thing we're going to do is download & enable the DevTools and Dialog modules, then (in a custom module) load DevTools, and create a menu callback for the page which will contain a link that'll trigger the modal:</p> <code>// example.module if (module_exists('devtools')) { devtools_load(); } /** * Implements hook_menu(). */ function example_menu() { $base = array( 'file' => 'example.pages.inc', 'file path' => drupal_get_path('module', 'example'), ); $items['dialog-example'] = array( 'title' => 'Dialog Example', 'page callback' => 'dialog_example_page', 'access callback' => TRUE, ) + $base; return $items; } </code> <p>As you can see, we're creating a page at the <code>/dialog-example</code> URI, specifying the <code>dialog_example_page()</code> function (in example.pages.inc) as the callback, and granting it unrestricted access.</p> <p>The page callback will look as follows:</p> <code>// example.pages.inc /** * Menu callback for /dialog-example */ function dialog_example_page() { $output = ''; drupal_add_library('dialog', 'dialog'); $trigger_id = 'example-dialog-trigger'; $output .= l(t('Open dialog'), 'dialog-example/form/start/' . $trigger_id, array( 'attributes' => array( 'class' => array( 'use-ajax', 'use-dialog', ), 'id' => $trigger_id, ), )); return $output; }</code> <p>In the callback we're loading the Dialog library, and then creating a unique (per page) ID for the link that'll trigger the modal to open (this is necessary so that we can easily target the link for HTML replacement on final form submission) and to make the form aware of this ID, we pass it as the last part of the URI.</p> <p>The <code>/dialog-example/form</code> URI will be created in our <code>hook_menu()</code>, and the <code>start</code> part of the URI specifies the initial form state that should be loaded (more on this later). You could add additional arguments to reference the content you're dealing with, user, or anything else that may be useful to your form. The only remaining tidbits is the assignment of <code>use-ajax</code> and <code>use-dialog</code> classes on the link, both of which are required.</p> <p>On to the <code>hook_menu()</code> for the addition of our new <code>/dialog-example/form</code> URI:</p> <code>// example.module if (module_exists('devtools')) { devtools_load(); } /** * Implements hook_menu(). */ function example_menu() { $base = array( 'file' => 'example.pages.inc', 'file path' => drupal_get_path('module', 'example'), ); $items['dialog-example'] = array( 'title' => 'Dialog Example', 'page callback' => 'dialog_example_page', 'access callback' => TRUE, ) + $base; $items['dialog-example/form'] = array( 'title' => 'Dialog Form', 'page callback' => 'dialog_example_form_ajax_delivery', 'page arguments' => array(2, 3), 'access callback' => TRUE, 'type' => MENU_CALLBACK, ) + $base; return $items; }</code> <p>The <code>dialog_example_form_ajax_delivery()</code> function will handle the delivery of the AJAX response, while passing through arguments <code>2</code> (form state) and <code>3</code> (trigger id) to the form.</p> <p><code>dialog_example_form_ajax_delivery()</code> is defined as follows:</p> <code>/** * AJAX menu callback for /dialog-example/form * @param [type] $state Requested form state. * @param [type] $trigger_id DOM id of AJAX-triggering element. * @return [type] [description] */ function dialog_example_form_ajax_delivery($state, $trigger_id) { $commands = dialog_show_form('dialog_example_form', $state, $trigger_id); ajax_deliver(array('#type' => 'ajax', '#commands' => $commands)); }</code> <p><code>dialog_show_form()</code> is, in simplified terms, a wrapper for Drupal's <code>drupal_build_form()</code> function, and will pass on all arguments following the form id (in our case, <code>dialog_example_form</code>) to the form builder.</p> <h3>The form</h3> <p>Our <code>$form</code> will be structured as three top-level state containers (beginning with the <code>state_</code> prefix), and a series of child elements for each state. The <code>$form_state</code> will be updated to contain various settings, and finally, <code>$form</code> and <code>$form_state</code> will be processed by <code>dialog_form_prepare()</code>.</p> <code>/** * The example form itself. * @return [type] [description] */ function dialog_example_form($form, &$form_state, $state, $trigger_id) { $form = array(); // start state $form['state_start']['name'] = array( '#title' => t('Name'), '#type' => 'textfield', '#required' => TRUE, ); $form['state_start']['submit'] = array( '#type' => 'submit', '#value' => t('Continue'), ); // second state $form['state_title']['title'] = array( '#title' => t('Title'), '#type' => 'textfield', ); $form['state_title']['submit_checkbox'] = array( '#type' => 'checkbox', '#title' => t('Continue'), ); // third state $form['state_final']['title'] = array( '#title' => t('What step is this?'), '#type' => 'radios', '#options' => array( '1' => t('First'), '2' => t('Second'), '3' => t('Third'), ), ); $form['state_final']['submit'] = array( '#type' => 'submit', '#value' => t('Final submit'), ); // dialog configuration $dialog_common_options = array( 'width' => '700px', 'position' => 'center', ); // state configuration $state_common_options = array( 'close_link' => TRUE, 'close_link_text' => t('Cancel'), 'close_link_class' => array( 'class-one', 'class-two', ), ); $form_state['dialog_settings'] = array( 'current_state' => $state, 'trigger_id' => $trigger_id, 'states' => array( 'start' => array( 'dialog' => array('title' => t('First state')) + $dialog_common_options, 'submit' => array( 'submit' => array( 'element' => &$form['state_start']['submit'], 'next_state' => 'title', ) ), ) + $state_common_options, 'title' => array( 'dialog' => array('title' => t('Second state')) + $dialog_common_options, 'close_link_text' => t('Different Cancel Text'), 'submit' => array( 'submit_checkbox' => array( 'element' => &$form['state_title']['submit_checkbox'], 'next_state' => 'final', ) ), ) + $state_common_options, 'final' => array( 'dialog' => array('title' => t('Third state')) + $dialog_common_options, 'close_link' => FALSE, 'submit' => array( 'submit' => array( 'element' => &$form['state_final']['submit'], ), ), 'submit_autoclose' => TRUE, ) + $state_common_options, ), ); dialog_form_prepare($form, $form_state); return $form; }</code> <p>The form declares the three top-level state containers and their children as regular FAPI elements. Each state has a child element that will be used to submit that particular state (for example's sake, the second state uses a checkbox).</p> <p>Finally, we're going to add validation & submit handlers with <code>drupal_set_message()</code> statements to see how our form submissions are processing:</p> <code>/** * Validation handler for dialog_example_form(). */ function dialog_example_form_validate($form, &$form_state) { $state = $form_state['dialog_settings']['current_state']; drupal_set_message(t('Validate for state: !state', array('!state' => $state))); } /** * Submit handler for dialog_example_form(). */ function dialog_example_form_submit($form, &$form_state) { $state = $form_state['dialog_settings']['current_state']; drupal_set_message(t('Submit for state: !state', array('!state' => $state))); }</code> <p>The dialog settings configured in <code>$form_state</code> specify each state, its title and an array of options passed to the actual <a href="http://jqueryui.com/dialog/" title="jQuery Dialog">jQuery Dialog plugin</a>. Furthermore, the <code>submit</code> key specifies an array of elements which trigger the state's form submission. The <code>element</code> key should contain a reference to the element within <code>$form</code>, while the optional <code>next_state</code> key specifies the next form state to load (this could be used to jump to any state); since this key is optional, a single form can be used for multiple "stand-alone" form states.</code> <p>By default, a 'Cancel' link is appended to each form state, but this can be disabled with the <code>close_link</code> key, and its text can be changed by using <code>close_link_text</code>.</p> <p>Some additional options include: <code>submit_autoclose</code>, <code>submit_redirect</code> (should be an absolute URL), and <code>submit_js_callback</code>, which takes an array with <code>behavior</code> and <code>method</code> keys that specify the JS method to call upon successful form submission. For example, the values</p> <code>'submit_js_callback' => array( 'behavior' => 'example', 'method' => 'form_submit_js_callback', ),</code> <p>will result in <code>Drupal.behaviors.example.form_submit_js_callback()</code> being called.</p> <p>If you're interested in seeing the internal details of how DevTools' Dialog component works, have a look at <code>devtools/components/dialog/dialong.inc</code>.</p> <p>This wraps up the process of creating multi-step forms in modals with quite a bit of additional functionality. If you have any questions, hit the comments below!</p></div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/drupal" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal</a></div><div class="field-item even"><a href="http://yuriybabenko.com/taxonomy/term/73" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">modal</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/form" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">form</a></div></div></div><div class="field field-name-field-file field-type-file field-label-above"><div class="field-label">Files</div><div class="field-items"><div class="field-item even"><span class="file"><img class="file-icon" alt="" title="application/zip" src="/core/modules/file/icons/package-x-generic.png" /> <a href="http://yuriybabenko.com/sites/default/files/blog/devtools_dialog_example.zip" type="application/zip; length=2186" title="devtools_dialog_example.zip">DevTools&#039; Dialog Example Module</a></span></div></div></div> Fri, 01 Nov 2013 20:40:00 +0000 yuriy 57 at http://yuriybabenko.com Introducing: Dev Tools http://yuriybabenko.com/blog/introducting-dev-tools <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p><a href="https://drupal.org/project/devtools" title="Dev Tools Module">Dev Tools</a> is a little-known module, which I published a little more than a year ago. As the description states, the module is a "collection of PHP classes and functions which help with and simplify Drupal module development." Sounds pretty vague, right? Hopefully this post will clear things up!</p> <h3>A little history & reasoning</h3> <p>Over the many years of Drupal development I accumilated lots of "helper" code, which I carried over with me from project to project. This code was always in a project-specific helper module, and it was a constant pain to rename the module and all associated hooks & files for every new project.</p> <p>The other pain I encountered was that every mid-large project required dozens of proprietary <em>projectname_foo, projectname_bar</em> modules, resulting in a messy-looking folder structure, and the constant need to write update hooks to enable/disable custom modules, ping other developers to run said hooks (if automation wasn't available), etc. The (experimental) idea behind Dev Tools was to write a single module that compartmentalized all functionality in a style similar to individual modules, and could be extended and built upon.</p> <h3>The components</h3> <p>The main Dev Tools module file acts like a gateway to individual components. The <em>.module</em> file contains regular Drupal hooks, but rather than implementing those hooks itself, Dev Tools looks for implementations of those hooks within each component, and then aggregates and returns the result to Drupal. This means that as far as Drupal is concerned, there is only one module, and in order to use a hook within a component, that hook must exist within the main <em>.module</em> file; these are the most important principles for any modules wishing to build upon Dev Tools.</p> <h3>Using Dev Tools</h3> <p>Dev Tools must be loaded from a custom module file in order for you to be able to use its components. The simplest way to do this is to place the following code at the top of your <em>.module</em> file, outside of any hooks:</p> <code>if (module_exists('devtools')) { devtools_load(); }</code> <p>... this will load all components within the Dev Tools namespace. Alternatively, you can load one or more individual components:</p> <code>if (module_exists('devtools')) { // load Debug component from the Dev Tools namespace devtools_load('devtools', 'debug'); }</code> <code>if (module_exists('devtools')) { // load Ajax & Debug components from the Dev Tools namespace devtools_load('devtools', array('ajax', 'debug')); }</code> <p>If your custom module builds upon Dev Tools and uses the same components pattern, you would replace the first argument with your module's namespace, but that's certainly not required to use Dev Tools at a basic level.</p> <p>Now that Dev Tools is loaded you can take advantage of the individual components by simply calling the function names. A small tidbit of helpfulness is that each component function within Dev Tools can be overridden by a custom implementation, by simply registering the name of your overriding function in a global variable.</p> <p>For example, say you were using the <em>Debug</em> component and wanted to override the <em>print_rr()</em> function to add more whitelisted IPs (which will then be allowed to see the debug messages, but that's details for another post). What you'd do is set the global variable <em>_devtools_handler_print_rr</em> to the name of your overriding function, and then declare that function. The same naming convention can be used to override any other Dev Tools function.</p> <code>if (module_exists('devtools')) { devtools_load(); $GLOBALS['_devtools_handler_print_rr'] = 'my_custom_print_rr'; } /** * Overrides devtools' print_rr function handler to add new IPs * @param [type] $data [description] * @param boolean $return [description] * @return [type] [description] */ function my_custom_print_rr($data, $return = FALSE) { $debug = \devtools\components\Debug::instance(); $debug->addIp('10.0.0.1'); // our new IP return $debug->output($data, $return); }</code> <p>I'll be going over some of the functionality of individual components in separate (upcoming) blog posts.</p> </div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/taxonomy/term/72" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">dev tools</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/drupal" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div></div></div> Tue, 22 Oct 2013 03:02:42 +0000 yuriy 56 at http://yuriybabenko.com When is Drupal not the right choice? http://yuriybabenko.com/blog/when-drupal-not-right-choice <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p>I've been developing with Drupal since the late 4.6 days (that's over seven years) and have been involved with hundreds of Drupal projects. Over this time I've been exposed to just about every type of project you can imagine, from mom & pop blogs, to government intranets, ecommerce sites, massive document libraries and social networks. While at the end of the day all projects got completed, some of them would have been much better-off being built on something other than Drupal.</p> <h3>What is Drupal?</h3> <p>Many people mistakingly equate Drupal with a CMS (Content Management System); while this may have been accurate in the very early days, it has not been so for quite a while. Drupal is primarily a framework, one that can be used to build a CMS, but out of the box it is nothing more than a toolset with some basic functionality - a codebase and some interface forms. Drupal is not a blogging platform, not an ecommerce store, not a forum, and not a social network, but it can be used to build all of these (and then some).</p> <p>With Drupal's recent vast adoption and general growth it can be very tempting to use it for everything, especially if that's your (or your company's) strength and comfort zone. An experienced Drupal developer can dissect the system, turn it sideways, and make it do things it was never even remotely intended for (while still following the best practices), but the tricky part is looking at every project objectively and deciding not whether you <em>can</em>, but whether you <em>should</em>.</p> <h3>The strengths</h3> <p>Drupal's tools are fantastic at structuring and managing content. The Entity, Field and Form APIs make managing content a pleasure, while modules like Taxonomy and Menu give us efficient ways of structuring, organizing, and filtering the content. Even on the presentation side of things we've got the flexible theme layer and tools like Display Suite to give us more options than anyone is likely to need. If your project's primary uses cases centre around creating, managing and viewing content, all this will combine for a great developer experience.</p> <h3>A large content base does not a content-driven site make</h3> <p>There's nothing wrong with managing millions of pieces of content within a Drupal site - this is very doable, has been done many times, and Drupal can be a good solution, but prior to starting the project you have to ask yourself, how will the site be used? If your users will be going through a relatively simple content creation flow, assigning content to containers, commenting and doing basic interactions with other site users, you're likely to get 90% of the way "there," with use & configuration of existing, third-party tools, and some straight-forward custom development for the remaining 10%. If this is the case, great, saddle up and go for it.</p> <p>On the other hand, if the project requires a complex content review flow, integration with a dozen third-party APIs, tricky UI elements, tons of user-driven events and tens of thousands of lines of custom modules to achieve your end goal, there's a good chance you're building an application and not a content-driven site. Take Twitter for example: massive amounts of content, but not content-driven; it's all about user interactions, not the tweets themselves.</p> <h3>The weaknesses</h3> <p>Among a myriad of small gripes and complaints (which any project has), there are two large, serious reasons to not build an <u>application</u> in Drupal.</p> <p><strong>Performance.</strong> Drupal has never been a fast performer; the philosophy of tailoring to everyone's needs resulted in large codebase, an overly normalized database and other selectively-unnecessary bloat to provide the desired flexibility. Any given project will often only take advantage of 30 - 40% (gut feeling estimate) of what Drupal is capable of out of the box. The remaining functionality does nothing but slow things down, and the architecture decisions made to achieve that functionality will also have impacted the performance of the parts you <em>are</em> using.</p> <p>The usual approach to dealing with Drupal's performance implications is to cache, cache, and cache some more. Everything from the database query results & the middleware, to front end resources & finally the request itself gets cached. If you are dealing with a content-driven site and a largely anonymous user base, this can be a cheap & effective solution (throw something like Varnish in front of your web server and you're ready to serve millions of requests), but with any application comes an authenticated user base, and caching suddenly isn't the answer to all your problems.</p> <p>Now you have to optimize a large codebase of contributed modules that you're likely not familiar with, and ones which were probably not tested in conjunction with the other modules you're using. Furthermore, to stick with the best practices, you're going to avoid modifying any contributed (or Drupal core) code, and try to work around the issues by tying into and modifying the flow & functionality from your custom modules. Welcome to your first nightmare, and one that directly leads to...</p> <p><strong>Development.</strong> Drupal development can be tricky. The programming itself is rarely anything "hard," but it takes a lot of effort & experience to understand the concepts, flow, and the implications of the changes you're making. Tasks which would be relatively trivial to implement in a custom application can take monumental amounts of work and add up quickly enough to affect the project's timeline, budget and end-of-the-day feasability.</p> <p>If you are a start up, building a proof of concept, or otherwise an application that is not 100% defined and may change at any moment (ie. a real agile project), Drupal is the last thing you want to be using. Pick a clean, lean, modern framework in whatever language you're most comfortable with and go at it - finding developers will be easier, iterating the project will be faster and simpler, and the entire life cycle will be a hundred times more natural. And of course, code bloat from unused functionality will be a hundred times smaller.</p> <h3>A real-world example</h3> <p>I'm currently part of a team developing a fairly large Drupal project that will end up supporting millions of users and pieces of content. Sounds like a perfect fit for Drupal, right?</p> <p>The main content type (let's say Article) has to go through a rigorous review process prior to being published and becoming accessible by end users. Here's the flow:</p> <ol> <li>User A creates a piece of content (which goes through a half-dozen custom UI tools and background integrations with external services) and submits it for review.</li> <li>User B comes along and performs review of type 1, by answering a series of questions (which can be added/subtracted/otherwise managed by the admin) and making a pass/fail decision after navigating through another couple UI widgets and modals.</li> <li>If User B failed the piece of content, it goes back to User A to re-start the flow, otherwise User B is instantly presented with the next review of type 2.</li> <li>Since the content passed review of type 1, two more users (C and D) must now come into the picture, manually start and complete their own type 2 reviews. Confirmation forms in modals everywhere.</li> <li>Once all three type 2 reviews have been finalized (have I mentioned the dozen modals in this process?), the system makes a decision based on the options the users have selected. The article can either restart the entire flow, get automatically published, or go to User E for review of type 3.</li> <li>User E fills out type 3 review and again, the system either re-starts the flow or publishes the content.</li> </ol> <p>Still following along? Now let's add things like automatically expiring non-completed reviews, scheduled and action-based notifications, ability to provide feedback to everyone involved in the review process (at pretty much any point in said process), a myriad of permission-related logic, and then multiply the entire thing a few times for some similar, but different flows.</p> <p>And to keep things interesting, requirements change on a daily basis.</p> <p>How's the Drupal fit looking now?</p> <p>This project is turning into an application that users use to interact with a large content base, and is certainly not your typical content-driven site. This is something that (ideally) should have been identified at the very start, and built on a framework that's more suitable for prototyping and rapid change.</p> <p>Building such applications on Drupal should be avoided, because unless your team is made up of expert developers with years of Drupal experience, you're quickly going to end up with an unmaintainable mess that will be impossible to develop further with anything resembling a positive return on dev investment.</p> <h3>tl;dr;</h3> <p>At the end of the day, it's in your (and your client's) best interest to scope out the projects properly, and pick the best tool for the job, which may not be the one you're most experienced with.</p> </div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/drupal" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal</a></div></div></div> Sat, 28 Sep 2013 22:54:58 +0000 yuriy 55 at http://yuriybabenko.com Badbot updated to support all forms http://yuriybabenko.com/blog/badbot-updated-support-all-forms <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p>Some time ago I <a href="http://yuriybabenko.com/blog/module-release-badbot-eliminating-spam-once-and-for-all">released</a> the <a href="http://drupal.org/project/badbot">Badbot</a> module. This module introduced a new way of blocking spambots, and the community response has been incredbly positive. Quite simply: it works.</p> <p>Unfortunately the 1.x version of Badbot only supported protecting the user registration form, and was not compatible with the dozens of other, equally vulnerable forms available for anonymous "consumption."</p> <p>Because of <a href="https://drupal.org/user/509892">Federico Jaramillo</a>'s efforts in the <a href="https://drupal.org/node/1868146">issue queue</a>, last night I was able to release Badbot 1.1, which adds support for any and all forms on a Drupal site (obviously including the much-desired comments form). This is a great example of a community contribution to an existing project, making big headway towards a cleaner, spam-free experience.</p></div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/captcha" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">captcha</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/spam" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">spam</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/drupal" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal</a></div></div></div> Thu, 22 Aug 2013 17:18:53 +0000 yuriy 54 at http://yuriybabenko.com From Drupal 7 to Laravel 3 and over to Drupal 8 http://yuriybabenko.com/blog/from-drupal-7-to-laravel-3-over-to-drupal-8 <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p>A few weeks ago I decided to rebuild this portfolio/blog site. The old site was running on Drupal 7, and I had long tired of the design; it was somewhat slow, too. Add to the mix a bit of boredom with the status quo and a desire to try something new and "lightweight," I downloaded the <a href="http://laravel.com" target="_blank">Laravel</a> (v3) framework I'd been hearing so much about.</p> <p>After flipping through a few tutorials and going through a large chunk of the <a href="http://laravel.com/docs" target="_blank">documentation</a> I had a pretty good idea of how things worked, as the setup & general ideas are fairly similar to a few MVC frameworks I've used before. I should mention that the documentation is quite good, however it's certainly aimed at experienced programmers. There are times when you need to read between the lines and put two and two together.</p> <p>I structured the app in a way that resembled Drupal: </p> <ul> <li>controllers all extend a base which provides a default layout</li> <li>base controller provides default variables for common elements</li> <li>controller actions override defaults as needed</li> <li>layout is assembled from a series of views for the page, regions, entities, and individual components (think: blocks)</li> </ul> <p> It's important to note that Laravel doesn't force you to structure your app in any specific way. For the most part, you can do whatever you want, which can be both a blessing if you know what you're doing, and a curse if you don't. </p> <p> By this point I've come to appreciate the flexibility of the routing system & restful controllers, and the intelligent class autoloading - nice! Route filters and Events function similar to Drupal's hooks, allowing you to tie into the processing at key points. Everything is simple, clean and flexible - so far, so good. </p> <p> On the front-end, I'd put together a simple design in Photoshop, implemented it with the <a href="http://twitter.github.com/bootstrap/" target="_blank">Twitter Bootstrap</a> CSS framework (Laravel has a nice <a href="http://bundles.laravel.com/bundle/bootstrapper" target="_blank">Bootstrapper</a> bundle), accounted for a simple responsive design, integrated <a href="http://fortawesome.github.com/Font-Awesome/" target="_blank">Font Awesome</a>, and got myself a <a href="https://typekit.com/" target="_blank">TypeKit</a> subscription (fantastic service) to get access to some nice fonts. </p> <p> With the look and feel in place I dug into the actual data: blog entries, projects and a few static pages. I really enjoyed working with Laravel's Migrations and Eloquent ORM. So simple, yet so powerful. Rolling database changes (forwards & back)? Sure. Want a model to access your content's database table? Just extend the <code>Eloquent</code> class. Need to create a relationship to another table and get full ORM support? One line of code. I could get used to this. </p> <p> Laravel also comes out of the box with a great CLI called "Artisan." Similar to Drush, this allows you to manage bundles (modules) and perform various administrative tasks. Very handy. </p> <p> Proceeding on with dev I set up image scaling/cropping (simplified version of Imagecache's functionality), DISQUS commenting, path aliasing, and basic taxonomy functionality. I have to admit that by this point the novelty of the framework had worn off, and the "why am I reinventing the wheel?" thoughts had firmly planted themselves in my head. I still had no admin backend, and the idea of building all those forms, permissions, and handling CRUD operations was daunting, to say the least. </p> <p> After some serious deliberation at such a late point in dev (I'd say I was close to 90% done), I cloned Drupal's 8.x branch and installed a local copy. I had gotten what I wanted out of Laravel (exposure to something new and upcoming, learning a few new tools and ways of approaching dev) and it was time to get back to what was a better option for my site: a proper CMS. (Sidenote: there are several CMS options built on Laravel which I didn't explore). </p> <p> This change in direction is not a negative for Laravel - it's a great framework, just not well suited to what I was building at the time. Much like most other frameworks, a lot of grunt work is necessary to get what Drupal provides out of the box (some of which can likely be alleviated with existing bundles). On the other hand, if those out of the box tools are not necessary (which would be the case for many web applications), Laravel would be a solid option. </p> <p> Back to Drupal: D8 is obviously in heavy development, but I've found it to be stable enough for a small, personal site and a pair of hands that aren't afraid to get dirty. Obviously there aren't any contributed modules which are ready for D8, but with Views in core, there's not much else that I'd want. In fact, the only thing I'm missing is Pathauto, but a basic custom solution can take care of that problem in half an hour. </p> <p> General development of content types, image presents, views, etc. was pretty straight forward. I quickly grew to like the new content editing UI, form validation helpers, and the admin menu. Submitting pieces of content does trigger an error, but everything still works, so nothing to worry about there. </p> <p> The first thing I noticed about my D8 install was how many fewer database tables there are. Wonderful cleanup from the bloat of D7, as many of those tables have "migrated" into configuration files located in <code>sites/default/files/config_[hash]</code>. </p> <p> Something to note: don't exclude this directory from your version control, as it's required, and will result in severe frustration and dents in your desk between the hours of 12am and 4am (because launching your newly completed site at 12am is a great idea). Additionally, the <code>[hash]</code> mentioned above is referenced in <code>sites/default/settings.php</code>, so make sure there's a match. On the other hand, the <code>sites/default/files/php</code> folder contains code which is environment specific and references absolute paths to various classes. </p> <p> After figuring out these little quirks, disabling the royal pain that is CKEditor (hoping that'll improve as D8 comes closer to full release), and setting up Varnish to speed up anonymous requests, the site has shown to be quite stable and useable. Plus, brownie points for using the latest and greatest, right? </p></div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/taxonomy/term/70" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">laravel</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/drupal-8" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal 8</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-7" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal 7</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal</a></div></div></div> Wed, 03 Apr 2013 01:35:10 +0000 yuriy 52 at http://yuriybabenko.com Module release: Badbot; eliminating spam once and for all http://yuriybabenko.com/blog/module-release-badbot-eliminating-spam-once-and-for-all <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p>Every webmaster has encountered spam; from account registrations, to contact form submissions, spam bots are constantly on the prowl for new targets. The usual approach to combating such bots is with one of the many available captcha tools, but they are all far from perfect, and with the abundance of services that have warehouses full of real people entering captchas all day (for spam bots to then use), they are often rendered useless. You can change the type of captcha used and you can add new ones every day, but that only leads to a game of cat and mouse with the spammers targeting your site, and wasted time on both sides, not to mention annoyed users. The simple fact is that captchas are not effective in combatting spam.</p> <p>Other alternatives to combatting spam include black lists of known spammers (such as stopforumspam.com), analyzing the submitted form&#39;s content, checking user agent strings, and the like, but again, none of the approaches works as well as one would like.</p> <p>To reliably identify spam bots we need to find something the bots do not have (but regular users do), and that something is JavaScript. Writing a JS engine is no simple task, and processing a page&#39;s JS seriously slows down the page load, and that&#39;s something spam bots simply cannot afford. If we can identify form submissions from clients which do not support JS, we can effectively block the spam bots. Sure, this will also affect users who have chosen to disable JS in their browsers, but the percentage of such users is very small, and I believe that inconveniencing a few users with an error message is an acceptable price to pay for eliminating spam.</p> <p>Enter:&nbsp;<a href="http://drupal.org/project/badbot">Badbot</a>.</p> <p>Version 1.0 of Badbot supports checking for JS on the user registration form. The logic is quite straightforward:</p> <ol> <li>find a required field in the form</li> <li>intercept the form submission and fire an AJAX request to generate a token based on the value of said field and a secret salt</li> <li>populate the returned token into a hidden field in the form and programmatically re-submit the form</li> <li>generate the token again in the form&#39;s validation handler, and compare it against the value of the hidden field in the form</li> <li>if the values do not match, block the form submission and display an error</li> </ol> <p>Without JS the AJAX request will not fire, the token will not be generated, and form validation will fail. Simple.</p> <p>I implemented this technique on one of Adobe&#39;s Drupal-powered websites a couple years ago. The site was receiving thousands of spam registrations on a daily basis, and none of the captchas made any difference. A new captcha would stop the spam, but the next day the spammers would tweak the bot and the registrations would start coming in again. It was obvious that there were real people solving the captchas. This JavaScript-based technique effectively blocked 100% of spam.</p> <p>I&#39;m long-overdue for writing this module, but now that the basic version is up on Drupal.org, I encourage everyone to try it out and share your thoughts. Have an idea? Is there something I missed and did not consider? Hit the comments below!</p> </div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/captcha" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">captcha</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/spam" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">spam</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/drupal" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal</a></div></div></div> Wed, 12 Dec 2012 04:54:50 +0000 yuriy 14 at http://yuriybabenko.com automatically exporting contexts http://yuriybabenko.com/blog/automatically-exporting-contexts <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p>A few weeks ago I wrote a helper module which allows you to automatically export contexts created via the <a href="http://drupal.org/project/context">Context</a> module into code, and then instantly provide them as default contexts. I&#39;ve just <a href="http://drupal.org/project/context_export">released</a> this module on Drupal.org.</p> <p>While many developers use the fantastic <a href="http://drupal.org/project/features">Features</a> module to export their contexts, that can lead to issues when a context contains blocks from a view, but that view is included in a different feature. When you add a context to a feature, Feature&#39;s dependencies functionality will bundle any views used in the context along with the feature, and if those views are included in a different feature, you will end up getting a conflict within the Features module. Context Export was written to avoid this issue, but still keep all contexts in code.</p> <p>Context Export should be configured at <code>/admin/structure/context/context_export</code> to specify the directory in which exported contexts should be stored; this directory must be writable by the server. While the module provides a link to export all overridden contexts on <code>/admin/structure/context</code>, there is also an option to automatically export contexts. When using automatic export, please note that contexts do not get exported as soon as you save the context configuration form, but rather on the next page load.</p> </div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/export" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">export</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/features" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">features</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/context" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">context</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-7" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal 7</a></div></div></div> Sun, 11 Mar 2012 22:42:02 +0000 yuriy 13 at http://yuriybabenko.com using user id arguments in drupal menu items http://yuriybabenko.com/blog/using-user-id-arguments-in-drupal-menu-items <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p>It is a very common need to be able to enter a menu path similar to user/[uid]/profile in the Drupal menu system ([uid] being a dynamic argument for the user&#39;s id), but that&#39;s not possible out of the box, and there are no modules which provide this functionality.</p> <p>Yesterday I got such functionality working by using the often-unknown <a href="http://api.drupal.org/api/drupal/developer--hooks--core.php/function/custom_url_rewrite_outbound/6" target="_blank">custom_url_rewrite_outbound()</a> function.</p> <p>The first step is to implement <code>hook_menu()</code> to define a menu path that will be used as an identifier for all argument-supported paths:</p> <code>/** * Implementation of hook_menu(); */ function yuba_menu() { $items = array(); $items['yuba'] = array( 'title' => 'None', 'page callback' => 'drupal_goto', 'page arguments' => array(''), 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); return $items; } </code> <p>What this path actually resolves to (<a href="http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_goto/6" target="_blank">drupal_goto()</a> in this case) doesn';t matter - it will never get called. The next part is to add this implementation of <a href="http://api.drupal.org/api/drupal/developer--hooks--core.php/function/custom_url_rewrite_outbound/6" target="_blank">custom_url_rewrite_outbound()</a> to your <code>settings.php</code> file:</p> <code>function custom_url_rewrite_outbound(&$path, &$options, $original_path) { /* Available tokens: user_uid -> currently logged in user account_uid -> uid of user being viewed, or node's author func_foo_uid -> will call foo_uid(), which should return the appropriate uid */ $tokens = array( 'user_uid', 'account_uid', 'func_(.*)_uid', ); // we don't do path modifications on the /admin pages. if (preg_match('#^yuba/(.+('.implode('|', $tokens).').*)$#', $path, $matches) && !preg_match('/^admin/', $_GET['q'])) { $path = $matches[1]; $uid = FALSE; switch($matches[2]) { // get uid from currently logged-in user case 'user_uid': global $user; $uid = $user->uid; break; // get uid from user or node being viewed case 'account_uid': if (preg_match('#user/([0-9]+)#', $_GET['q'], $inner_matches)) { $uid = $inner_matches[1]; } elseif (preg_match('#node/([0-9]+)#', $_GET['q'], $inner_matches)) { if ($node = node_load($inner_matches[1])) { $uid = $node->uid; } } break; // let callback function determine uid default: $func = str_replace('func_', '', $matches[2]); if (function_exists($func)) { $uid = (int) $func($original_path, $path); } break; } if ($uid) { $path = str_replace($matches[2], $uid, $path); } } }</code> <p>This code looks for all paths beginning with the &#39;yuba&#39; prefix (the one we defined in our custom hook), and modifies them, depending on the specified argument.</p> <p>The available arguments are <em>account_uid</em>, <em>user_uid</em>, and <em>func_my_module_uid</em>.</p> <p>Examples in paths:</p> <dl> <dt>yuba/foo/<em>account_uid</em>/bar</dt> <dd>This will replace the token with the UID of the user being viewed (if on a <code>/user/##</code>) page, or if you're on a node page (<code>/node/##</code>), the UID of the node's author</dt> <dt>yuba/foo/<em>user_uid</em></dt> <dd>This will replace the token with the UID of the currently logged in user (in the global <code>$user</code> object)</dd> <dt>yuba/foo/<em>func_my_module_uid</em>/bar</dt> <dd>This will replace the token with whatever value is returned by the function <em>my_module_uid</em>, which will recieve two arguments (<code>$original_path</code>, and <code>$path</code>); this allows for custom functions that determine what UID is to be used.</dd> </dl> </div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/tutorials" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">tutorials</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/user-id" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">user id</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/drupal-6" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal 6</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/uid" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">uid</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/menu" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">menu</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/argument" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">argument</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/token" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">token</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal</a></div></div></div> Tue, 30 Aug 2011 20:04:30 +0000 yuriy 12 at http://yuriybabenko.com drupal's domain access and sites in subfolders http://yuriybabenko.com/blog/drupals-domain-access-and-sites-in-subfolders <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p>Domain Access is a module which allows you to simulate Drupal's internal multi-site functionality; it is easy to set up, and even easier to use. This is (by far) the simplest way of sharing content (and users) between multiple sites. One of DA's downfalls is that it does not work with subsites in subfolders, meaning the structure of <code>site.com</code> and <code>site.com/subsite</code> is not supported. Due to very specific client needs, my project required just that - subsites in subfolders. "Not supported" was not an acceptable answer, so... here is my solution:</p> <p>Start off by enabling Domain Access (& other related modules that come in the package) and setting up a new domain under <code>Site Building -> Domains</code> - set the new domain up as a subdomain, so you end up with <code>site.com</code> and <code>subsite.site.com</code>. You should now create the appropriate <code>subsite.site.com</code> domain record in your vhosts file, or otherwise create the subdomain to point to your main (<code>site.com</code>) Drupal installation.</p> <p>The next step is adding this little bit of magic at the top of your <code>index.php</code> file. Yes, we're modifying a core file, but it's not too bad:</p> <code> //add folders in which the subsites reside to this array $sites = array( 'subsite', ); $request = explode('/', trim($_SERVER['REQUEST_URI'], '/')); if (sizeof($request) && preg_match('/^'.implode('|', $sites).'$/i', $request[0], $matches)) { //set the appropriate HTTP_HOST to trick Domain Access $site = $matches[0]; $_SERVER['ORIGINAL_HTTP_HOST'] = $_SERVER['HTTP_HOST']; $_SERVER['HTTP_HOST'] = $site.'.'.$_SERVER['HTTP_HOST']; //rebuild the Q if (!empty($_GET['q'])) { $q = implode('/', $request); if(($pos = strpos($q, '?')) !== FALSE) { $q = substr($q, 0, $pos); } $size = strlen($site); $_GET['q'] = ltrim(substr($q, $size), '/'); $HTTP_GET_VARS['q'] = $q; } //used to set the correct $base_url in settings.php global $da_site; $da_site = $site; } </code> <p>(On my project I actually have this in an external file and simply added an include in <code>index.php</code>.)</p> <p>Now we need to update <code>settings.php</code> in your <code>sites/default</code> folder:</p> <p>Find the line which contains the default <code>$base_path</code> code and replace it with this:</p> <code> global $da_site; if ($da_site) { $base_url = 'http://'.$_SERVER['ORIGINAL_HTTP_HOST'].'/'.$da_site; } </code> <p>Also uncomment the <code>$cookie_domain</code> variable and set it to your site's root: <code>$cookie_domain = 'site.com';</code></p> <p>... this will share sessions between your main site and subsites, so when a user logs in to one site, they're logged in to all sites. If you skip this step, users will not be able to log in on your subsites.</p> <p>Don't forget to add the appropriate configuration to <code>settings.php</code> for the Domain Access module! See DA's readme/install files.</p> <p>The last step is updating your <code>.htaccess file</code>.</p> <p>Add this:</p> <code> # DomainAccess-related # Removes the site prefix to ensure file paths still work RewriteRule ^subsite(.*) /$1 [L,QSA] </code> <p>Right above:</p> <code> # Rewrite URLs of the form 'x' to the form 'index.php?q=x'. </code> <p>That's it! Your Drupal site will now be tricked into thinking it's running on the subsite.site.com subdomain whenever you access <code>site.com/subsite</code>.</p> <p><strong>NOTE:</strong> This functionality has undergone <em>very limited</em> testing, so it's entirely possible that there may be issues. Please post your results!</p></div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/drupal" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/domain-access" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">domain access</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/multi-site" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">multi site</a></div><div class="field-item even"><a href="http://yuriybabenko.com/subfolder" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">subfolder</a></div></div></div> Fri, 15 Oct 2010 03:36:42 +0000 yuriy 10 at http://yuriybabenko.com corrupted drupal theme registry breaking all forms http://yuriybabenko.com/blog/corrupted-drupal-theme-registry-breaking-all-forms <div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even" property="content:encoded"><p>I just encountered a strange issue while switching between two development branches of the same Drupal site. On branch A the site worked normally, but on branch B none of the forms contained any fields. After much hair pulling, I narrowed down the difference between the two branches to a <a href="http://api.drupal.org/api/function/drupal_rebuild_theme_registry/6" target="_blank">drupal_rebuild_theme_registry();</a> call in my theme's template.php file on (the working) branch A, and realized what was going wrong.</p> <p>Calling <code>drupal_rebuild_theme_registry();</code> rebuilds Drupal's internal theme registry, and it's helpful to always do that during development, which is why this call was hardcoded in template.php. My broken branch B on the other hand, was actually the "production" branch, and did not have this call. When I added some form theming functions on branch A, those changes got written to the theme registry. After swapping to branch B, the database still contained the theme registry with the new changes from branch A, but since there was no longer a call to rebuild the theme registry and the new theming functions were nowhere to be found, the site effectively broke. Rebuilding the theme registry while on the branch B codebase solved the issue!</p></div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags</div><div class="field-items"><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal-planet" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal planet</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/fapi" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">fapi</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/form" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">form</a></div><div class="field-item odd"><a href="http://yuriybabenko.com/blog/tag/theme-registry" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">theme registry</a></div><div class="field-item even"><a href="http://yuriybabenko.com/blog/tag/drupal" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">drupal</a></div></div></div> Wed, 14 Jul 2010 02:01:28 +0000 yuriy 9 at http://yuriybabenko.com