Feb272013

PHP Conference Argentina

I am co-organizing PHP Conference Argentina. Based on the impressive speaker lineup, and the community support we have received, I believe this conference will not only be a huge deal for Argentina, but for Latin America as a whole.

When and where?

It’s a two day conference, celebrated 4 and 5 of October. We chose Buenos Aires not only because it is the most popular tourist destination in South America, but because it is home of the most active startup scene in the region.

Where do I get my ticket?

Tickets are up for sale at awesome discounts. Go get yours before they are sold out!

Who’s coming?

Check the speaker lineup. In the meantime here are some of our speakers: Rasmus Lerdorf (creator of PHP), Igor Sysoev (creator of NGINX), Derick Rethans (creator of Xdebug), Brian Doll (VP of Marketing at GitHub), Daniel Rabinovich (CTO of MercadoLibre), Sebastian Bergmann (creator of PHPUnit), Benjamin Eberlei (lead of Doctrine2), Nate Abele (creator of Lithium), Jordi Boggiano (creator of Composer), and Pablo Godel (founder of ServerGroove).

Why is it a big deal?

2012 showed that Argentina was the right place to host IT events, fact confirmed by the success of both RubyConf AR and JSConf.ar. It only made sense to not only offer a bigger, more important event this year, but to target web development as a whole.

That’s right. You don’t have to be using PHP to enjoy PHP Conference Argentina. We will be offering general web development talks for every PHP talk, so this is an event you don’t want to miss!



May262012

Extracting g11n translation strings in Lithium

If you are like me, you are happy, and you use Lithium. If you are not like me, well then, it sucks to be you. I personally love the Lithium-Doctrine2 (by means of li3_doctrine2) combo. It suits my needs, and gives me peace of mind. Sort of like a good glass of scotch at the end of the day. I don’t drink scotch, by the way.

Anyway if you are aiming for dominating the world, you need internationalization in your app. It is quite easy to do so with lithium. Go check the documentation, I’m not here to teach you about it. What I am here to do is fill a gap in li3′s proposal for g11n: extracting your translation strings and putting them in a nice gettext POT, ready for translation. I’ve built a dead-simple python script that gets the job done. Call it with the typical help argument and you’ll get:

$ scripts/g11n.py --help
usage: g11n.py [-h] [-a APP] [-o OUTPUT_FILE]

Globalization extract

optional arguments:
  -h, --help            show this help message and exit
  -a APP, --app APP     Application directory
  -o OUTPUT_FILE, --output-file OUTPUT_FILE
                        Where to write template

So you basically specify it the application directory, and where to write the template file. It will go through your application directory (skipping only the resources/g11n/ and resources/tmp paths) looking for any PHP files, and extract the strings. However it does not come so easy. If you are using Lithium’s built in translation lambdas ($t and $tn in your views) it will not work. I personally set a couple of function aliases in my app/config/bootstrap/g11n.php file, so that I could use the same translation functions regardless if I’m on a view, a controller, entity, or whatever else. These two aliases look simple enough:

function _t($message, array $options = array()) {
	return \lithium\g11n\Message::translate($message, $options + array(
		'default' => $message
	));
}

function _tn($message1, $message2, $count, array $options = array()) {
	return \lithium\g11n\Message::translate($message1, $options + compact('count') + array(
		'default' => $count == 1 ? $message1 : $message2
	));
}

So if you start using the above _t() and _tn() functions you can use my python script. It uses a well established tool known as xgettext for helping out with the extraction, and msgmerge for merging. The script source code is:

#!/usr/bin/env python

import argparse, os, re, subprocess, sys, tempfile

if __name__ == '__main__':
	parser = argparse.ArgumentParser(description='Globalization extract')
	parser.add_argument('-a', '--app', default='app/', help='Application directory')
	parser.add_argument('-o', '--output-file', help='Where to write template')
	args = parser.parse_args()
	args.app = os.path.abspath(args.app)

	if not args.output_file:
		args.output_file = args.app + '/resources/g11n/default.pot'
	if not os.path.isdir(args.app) or not os.path.exists(args.app):
		print('{i} is not a valid directory'.format(i=args.app))
		sys.exit(1)

	print('Preparing to extract strings from {i} and store them in {o}'.format(
		i = args.app,
		o = args.output_file
	))

	dirs = os.listdir(args.app)
	skipPaths = [
		'resources/g11n',
		'resources/tmp'
	]
	count = 0
	o = tempfile.NamedTemporaryFile(delete=False)
	for root, dirs, files in os.walk(args.app, followlinks=True):
		d = re.sub(r'^' + re.escape(args.app + '/'), '', root)
		skip = False
		for skipPath in skipPaths:
			if re.match(r'^' + re.escape(skipPath) + '/?([^/]*)', d):
				skip = True
				break
		if not skip:
			for f in files:
				if re.search(r'' + re.escape('.php') + '$', f):
					o.write((os.path.join(root, f) + '\n').encode('utf-8'))
					count += 1
	o.close()

	print('Found {c} files to process'.format(c=count))

	if count > 0:
		output = args.output_file
		backup = None
		if os.path.exists(args.output_file):
			backup = args.output_file + '.old'
			output += '.new'
			os.rename(args.output_file, backup)

		subprocess.call([
			'xgettext',
			'--files-from=' + o.name,
			'--output=' + output,
			'--keyword',
			'--keyword=t',
			'--keyword=_t',
			'--keyword=tn:1,2',
			'--keyword=_tn:1,2',
			'--language=PHP',
			'--from-code=\'utf-8\'',
			'--package-name=\'workana\'',
			'--copyright-holder=\'Workana\'',
			'--package-version=\'1.0.0\'',
			'--msgid-bugs-address=\'g11n@workana.com\''
		], stdout=None, stderr=subprocess.STDOUT)

		if os.path.exists(output):
			newOutput = output + '.sed'
			with open(newOutput, 'w') as f:
				subprocess.call([
					'sed',
					's/\(Content-Type:\s*text\/plain;\s*charset=\)CHARSET/\\1UTF-8/i',
					output,
				], stdout=f, stderr=subprocess.STDOUT)
				if os.path.exists(newOutput):
					os.rename(newOutput, output)

		if backup:
			print('Merging with existing template')
			subprocess.call([
				'msgmerge',
				'-i',
				'-N',
				'-o',
				args.output_file,
				backup,
				output
			], stdout=None, stderr=subprocess.STDOUT)

			os.unlink(output)
			os.unlink(backup)
	os.unlink(o.name)


Mar112012

Persistent localized routes with Lithium

Just a few minutes ago I got pinged through IRC asking if I could provide a quick answer. The question asked by m99 (thanks for the blog post inspiration!) was if there was an easy way to ensure localized routes with Lithium. As with any challenge you face with lithium, the answer ended up being dead simple.

So let’s make sure we understand what we want to do first by enumerating a list of objectives:

  • We want to make sure that our application URLs are prefixed with a locale that identifies the language, and region of choice.
  • If there is no locale in the URL, we need to figure out what the best choice is for the current visitor, and point them to the same URL they are intending to reach, but with the locale prefix added.
  • We want to make this prefix persistent (search engine mumble jumble.) So once a locale prefix is set, all links within the application should contain the prefix.

Believe it or not, all this is pretty much offered out-of-the-box using nothing but the following Lithium features

So let’s get to work. First thing we want to do is ensure that the g11n.php file is loaded as part of the bootstrap process. So edit your app/config/bootstrap.php file and uncomment the line that requires the app/config/bootstrap/g11n.php file.

Next we want to define which languages are supported in our application. Normally the list of supported languages is defined in the g11n.php file through environment variables, as you can then use that to build up a language menu in your views, or test more languages on different environments. Edit the languages defined in your app/config/bootstrap/g11n.php file. For example my list of languages looks like the following:

$locale = 'en_US';
$locales = array(
	'en_US' => 'English',
	'es_AR' => 'Español'
);

Environment::set('production', compact('locale', 'locales'));
Environment::set('development', compact('locale', 'locales'));
Environment::set('test', array('locale' => 'en', 'locales' => array('en' => 'English')));

Now we want to make sure we allow locales to be specified as part of an URL. Edit your app/config/routes.php file and add the following route right before any of your custom routes (so it should pretty much stand as the first route defined):

Router::connect('/{:locale:[a-z]{2}_[A-Z]{2}}/{:args}', array(), array('continue' => true, 'persist' => array('locale')));

We now need to make sure that if there is no locale specified, or if the locale is not valid, we prefix the current action with a valid locale. We also want to be smart and figure out what’s the best language for the current request, and if there is a language match, but not a region match, we still maintain their desired language (for example, in our g11n.php file we defined es_AR as the only spanish localization, so if someone with an es_ES locale shows up, we still want to take them to a Spanish version of our website.)

Go back to edit your app/config/bootstrap/g11n.php file, and look for a closure bound to a variable named $setLocale right at the very bottom. This closure normally looks concise, but replace it all with the following:

UPDATE: The best matching code that was taking care of our example es_ES -> es_AR matching scenario has been greatly simplified by the recent pull request merged into Lithium on 6c95cb80bc

$setLocale = function($self, $params, $chain) {
	$redirect = false;
	$locales = array_keys(Environment::get('locales'));
	if (!$params['request']->locale()) {
		$redirect = true;
		$locale = Locale::preferred($params['request'], $locales);
	} else {
		$locale = $params['request']->locale();
	}

	if (empty($locale) || !in_array($locale, $locales)) {
		$redirect = true;
		if (!empty($locale)) {
			try {
				$locale = Locale::lookup($locales, $locale);
			} catch(\InvalidArgumentException $e) {
				$locale = null;
			}
		} else {
			$locale = null;
		}

		if (empty($locale)) {
			$locale = Environment::get('locale');
		}
	}

	if ($redirect) {
		$params['request']->locale($locale);
		$url = compact('locale') + $params['request']->params;
		return function() use($url) {
			return new \lithium\action\Response(array(
				'location' => $url
			));
		};
	}

	Environment::set(true, array('locale' => $params['request']->locale()));
	return $chain->next($self, $params, $chain);
};

Let’s do a quick summary of what the code above does:

  • Lines 02-08: try to get the locale from the current prefix. If no prefix is provided, do it by figuring out what would be the best locale for the current request.
  • Lines 11-26: what to do if there is no locale (something is weird in the request and we can’t figure out the best locale for the client.), or if the locale specified is not within our list of active locales. The interesting part here are lines 13-21, where we do the best-match discovery I mentioned earlier. So if someone is requesting an en_UK locale, and we only have en_US, we make them use en_US. This block of code ends with a simple fail over: if no locale was found to be best for our client, we use the default locale (lines 23-25)
  • Lines 28-36: If we have figured a new locale, we redirect the user to the current URL, but setting the new locale.

That’s it! Try accessing your application with no locale defined. For example if you try to reach http://myapp/signup you will be redirected to http://myapp/en_US/signup, and all your links will be prefixed with the en_US.



Feb132012

Responsible buying

You may be asking what prompted me to write about anything other than code (disregarding the fact that my last post was written quite a while ago.) Let me tell you. I often debate general issues on my twitter account (sometimes in english, sometimes in my mother tongue, spanish.) Some of these debates end up being quite productive, prompting me to further think about the discussion at hand, or to even change some of my behaviors.

This time is the latter. I was, as it is not uncommon in me, tweeting about how questionable Apple policies are when it comes to the work environment in their factories, when Felix, a person who I have the strongest respect for, tweeted something that got my attention:

by that logic, you are responsible or every action taken by every company you purchase from

My quick response to him is a thought I wish to ellaborate:

and you are. thats the point im making. we should do our best to avoid supporting companies that WE KNOW are being evil

If one would boycott all companies that are, at one level or another, doing evil, one would probably have to live alone, in the middle of the desert, consuming nothing but vegetables. I know that’s not practical. However, we have to draw the line somewhere, right? And don’t you think that a company that supports, by action or inaction, job environments that are universally accepted as inhuman is somewhere near the line? To give you a bit of background, look at this interview Bill Maher did shortly ago with Mike Daisey (it’s less than 8 minutes long, you have the time):

I’m sure I am backing other companies that may be acting immorally somewhere around the globe, but if I know they are, I will do my best to avoid giving them business, and so should you. If I knowingly give them business I am partly responsible for their wrong doings. I know that’s not something you’d like to hear, but that’s a fact.

There will be times where it feels almost impossible to take your business elsewhere. Apple lovers will tell me (and they would mostly be right) that there is no better phone than the iPhone. That the iPad is miles ahead of any other tablet. I know, I get it. They build nice products. But at what cost? Would you rather buy the nice cars that come out of a factory in Nazy germany, or would you like to help Germany (and the rest of the world) free itself from the Nazi horror? I prefer to do the later. Now, please, don’t panic, I am NOT saying Apple is pro-Nazi (that’s a comparison that has been largely misused), that’s just an extreme example to make a point. I am saying that we all know that they are doing something that’s quite wrong, so we should do our best to stop supporting them.

So even when I always thought I should be a responsible buyer, I am now making an extra effort: I will try to inform myself, to the best of my abilities, about what lies behind the product I’m buying. If it is a sweat shop in Asia, I’m out. If it’s cheap because it’s based on child labor, I’m out. I am morally responsible for my purchases, so I will act accordingly. I may sometimes, inadvertently, support evil companies. If so, I wish to have the capacity to find out about it, and stop supporting those companies.

Let us do this together, shall we?



Jan032012

Route middleware with Lithium

I spend most of my day switching between languages. Sometimes I start the morning with an early dose of LUA, then I get a lot of Python and C++, followed by the yummy dessert that is Node.js. But there’s always room for PHP. So let’s talk about PHP today.

People often think that when you are coding PHP you have to do things “the PHP way”. Well, let me clear it up for you: there’s no such thing as “the PHP way”. If there’s something that defines PHP is its flexibility to be as ugly or as beautiful as you want it to be. With that in mind, what prevents you from taking the lessons learned in one language to another?

That’s the premise I always have when I’m coding. Let me give you a for instance. On Node.js, I normally use express.js as a framework. Check it out, it’s pretty awesome. One of the things I love in express.js, is its route middleware capabilities. When I code in any PHP framework, that’s one of the things I miss the most.

Not many PHP frameworks are flexible enough to support such concepts. One of them is, though. Lithium has many of the things I love about PHP (5.3+ obviously, as I consider anything < 5.3 a waste of my time lately), and some of the things I love about other languages. One of them is Lithium’s addiction to closures. I love it. They allowed me to take the concept of express.js’ route middleware and apply it to my PHP code.

Let’s start with a dummy application skeleton. A users table:

CREATE TABLE `users`(
    `id` INT NOT NULL AUTO_INCREMENT,
    `email` VARCHAR(255) NOT NULL,
    `password` VARCHAR(255) NOT NULL,
    PRIMARY KEY(`id`)
);

INSERT INTO `users`(`email`, `password`) VALUES(
    'test@email.com', '$2a$04$U7qYPVYq2YBxqfHL8F2pteERxQYwLTVtAjMIh48Lef9sLSiMVtGHy',
    'john@email.com', '$2a$04$U7qYPVYq2YBxqfHL8F2pteERxQYwLTVtAjMIh48Lef9sLSiMVtGHy'
);

In your app/config/bootstrap/connections.php, make sure you uncomment the default database and hook it up to the database owning the table we just created. We’ll also be using sessions, so uncomment the session.php reference in app/config/bootstrap.php. You may have noticed that our initial users have a password set to a specific value, which means a specific salt was used (the password hashed there in plain text is ‘password’, minus the quotes.). So go ahead and add the following to your app/config/bootstrap/session.php file:

use lithium\security\Auth;
use lithium\security\Password;

$salt = '$2a$04$U7qYPVYq2YBxqfHL8F2pte';
Auth::config(array(¬
    'adapter' => 'Form',
    'model' => 'Users',
    'filters' => array('password' => function($text) use($salt){
        return Password::hash($text, $salt);
    }),
    'validators' => array(
        'password' => function($form, $data) {
            return (strcmp($form, $data) === 0);
        }
    ),
    'fields' => array('email', 'password')
));

The salt was generated with a call to \lithium\security\Password::salt('bf', 4). I always use blowfish for password hashing (and 2^16 iterations in production). If you don’t use blowfish, here’s why you should. Anyway so you may want to store the hash on a better, configurable approach. I opted for a simple variable for this example. Once the salt is defined, I went ahead and configured Auth to use lithium’s Password::hash() method for hashing using the generated salt, and telling it how to compare hashed passwords against the database value. Pretty simple.

Let’s now build the Users model. It won’t have anything in there, really. So just create your app/models/User.php file with the following contents:

<?php
namespace app\models;

class Users extends \lithium\data\Model {
}
?>

Now the controller. Create a file named app/controllers/UsersController.php with the following contents:

<?php
namespace app\controllers;

use lithium\security\Auth;

class UsersController extends \lithium\action\Controller {
    public function login() {
        if (!empty($this->request->data)) {
            $user = Auth::check('default', $this->request);
            if ($user) {
                $this->redirect(array('action' => 'view', 'id' => $user['id']), array('exit' => true));
            }
        }
    }

    public function logout() {
        Auth::clear('default');
    }
}
?>

Nothing really complicated there. Don’t forget the view in app/views/users/login.html.php:

<?php
echo $this->form->create();
echo $this->form->field('email');
echo $this->form->field('password', array('type' => 'password'));
echo $this->form->submit('Login');
echo $this->form->end();
?>

That should give you a working login / logout. Add some dummy actions to the UsersController.php file:

public function view() {
    echo 'view';
    $this->_stop();
}

public function edit() {
    echo 'edit';
    $this->_stop();
}

Ok now we are ready to play with some route middleware. What we want to achieve is the following:

  • No action named edit, on any controller, should be accessible without a logged in user.
  • When accessing either the Users::edit or Users::view action, there should be an ID specified as a route parameter, and it should match an existing User record.
  • When accessing the Users::edit action, the given user should match the currently logged in user.

These are pretty basic security checks that you would normally put on the controller. Not this time. Edit your app/config/routes.php file and add the following right below the use statements found at the beginning of the file:

use lithium\net\http\RoutingException;
use lithium\action\Response;
use lithium\security\Auth;
use app\models\Users;

These are all classes that we will use in our route middleware. Let’s start with the first checkpoint we want to achieve: “No action named edit, on any controller, should be accessible without a logged in user“. Add the following to the routes.php file, below the content we just added:

Router::connect('/{:controller}/{:action:edit}/?.*', array(), array(
    'continue' => true,
    'handler' => function($request) {
        if (!Auth::check('default')) {
            return new Response(array('location' => 'Users::login'));
        }
    }
));

The first parameter (continue) ensures that this route definition is treated as a continuation route. This is because we don’t want to interrupt any normal route / parameter processing in this definition. We just wanna “grab” all calls to any edit action, and check (using Auth) for a valid user. If none is found, we process the request by returning a Response, which in the end redirects the user to the login page. If there is indeed a logged in user, the router will continue looking for other route definitions to match the request. So now all edit actions require a logged in user. Cool.

Next in our list: “When accessing either the Users::edit or Users::view action, there should be an ID specified as a route parameter, and it should match an existing User record.” Add the following to the routes.php file, below the content we just added:

Router::connect('/{:controller:users}/{:action:edit|view}/{:id:\d*}', array('id' => null), function($request) {
    if (empty($request->params['id'])) {
        throw new RoutingException('Missing ID');
    } elseif (!Users::first(array('conditions' => array('id' => $request->params['id'])))) {
        throw new RoutingException('Invalid ID');
    }
});

We are now getting more serious. In this definition, we are only matching the Users controller, and actions named either edit or view, which may or may not contain an id parameter. The route handler first checks to make sure the id parameter is given (if not, a RoutingException is thrown.) If the parameter is specified, it is used to find a matching User record with the given ID. If none is found, yet another RoutingException is thrown (you may wish to do something different here, like ensuring a 404 status). If the user is found, the route is not handled, which means some other route definition will handle it (the default route, in this case.)

The final checkpoint we have is: “When accessing the Users::edit action, the given user should match the currently logged in user.” So add the following to the routes.php file, below the content we just added:

Router::connect('/{:controller:users}/{:action:edit}/{:id:\d+}', array('id' => null), function($request) {
    $user = Auth::check('default');
    if ($user['id'] != $request->params['id']) {
        throw new RoutingException('You can only edit your own account');
    }
    return $request;
});

This defines a specific match to the Users::edit action with a set id parameter. We use that parameter to make sure it matches the ID of the logged in user. If it doesn’t match, we throw a RoutingException. If it does match, we return the request as we have successfully processed it.

You can now try accessing the edit and view actions using different scenarios: with a logged in user, while being logged out, editing a user which is not the current logged in user, etc. Everything should be nicely protected. And yet our controller code remained untouched. Nice, huh? That’s routing middleware for you. :)



Mar302011

Book release: CakePHP 1.3 Application Development Cookbook

Just a few days ago, I was happy to see my first book published. Entitled CakePHP 1.3 Application Development Cookbook, it’s a book released in the form of a cookbook, with a series of solutions to common problems one faces when developing CakePHP applications.

While working on it, I tried to aim for developers at different levels of knowledge, yet a disclaimer has to be made: this is not a beginners book. It will not teach you how to install CakePHP, or how to get its friendly URLs working on Microsoft platforms (dodged that bullet.) It is written for CakePHP developers that are looking to solve different problems, and leverage their own applications. So no “building a blog” chapter in this book.

There are some recipes that deal with more complex topics, while others deal with what I consider interesting solutions to simple problems. Each recipe starts by proposing a problem, showing the solution, and giving an explanation of how the solution works. Most of the recipes include alternatives and extend the topic at hand beyond the scope of the problem they are solving, and some of them are based on open source packages that CakePHP community members (myself included) released.

This book also benefited from an unbelievably, super-cool, top of the world team of technical reviewers (the CakePHP 1.3 lead developer happens to be amongst them) that made its code shine (I am known for being humble.) They improved each recipe and proposed awesome alternatives to my original ideas. Because of that, this blog post is being written. I’m not sure sure I would’ve been as proud of the original version of the book ;)

You should also know that the publishing company behind the publication of this book, Packt Publishing, is donating an important portion of the book earnings to the Cake Software Foundation, which is like The Force behind CakePHP. So I might not get rich, but at least the foundation will get some beers out of each sale. And trust me, nothing says thank you like a beer.

If you bought the book, I welcome any feedback you may have (you could also leave a review and tell others how super cool the book is.) If you are more of a bytes person and less of a paper person, and look forward to reading the digital version, you can also get the ebook.



Aug202010

File uploading with multi part encoding using Twisted

If you program in Python and you are building web clients you need to know about Twisted. It is perhaps the most flexible and powerful Python framework for building web clients and servers, and has been proven to work wonderfully under heavy loads. So powerful, that the most basic tasks tend to be a bit of a pain. This is what I realized when I found myself in the need to upload large files to a web site using Twisted. I googled, and I googled, and I found no real answer to my problems.

You can of course find an easy solution: build a multipart request by building the request body yourself out of the files you are trying to upload. Sure, that works. But what happens when you are dealing with a 50 MB file upload? What about 500 MB? I’m sure you are not planning to encode the whole file as a string, right?

This is where Twisted’s body producers come handy. Implementing your own body producer, you have total control on how to build the request. In fact, Twisted will call you every time it needs data for the request, so you can be sure you won’t be building the whole chunk in one string. Instead, you will be sending chunks of bytes to what is known as the consumer. What is the consumer? Whatever is asking for a request body.
Continue Reading »



Jan022010

Building a blog with Lithium and Doctrine

This post aims to be a very basic introduction to the world of Lithium, also known as li3. As such, it is mostly based on the ubiquitous blog sample. The idea is to learn, through the source code, the basic notions on Lithium and its integration with the Doctrine ORM mapper to build a very basic blog application. Unlike the blog example at rad-dev.org, this example is based on Doctrine 2.0, which is (at the time of this writing) in Alpha stage, and is built for PHP 5.3. After this tutorial, you should be able to jump start and build your own li3 powered applications.

Before we proceed, a disclaimer. This tutorial shows a rather quick and dirty integration with Doctrine (as you’ll see it’s all mostly done in a base model class), while the most clean approach would be to implement all this as an extension, using Datasources and Query parsing. I’m happy to say this is all being done as we speak (thanks kuja!), so very soon you’ll find an even better approach to Doctrine integration.

Continue Reading »



Apr282009

Pagination with custom find types in CakePHP

With the release of CakePHP 1.2 a whole set of new features were made available to us bakers. One of those features is custom find types, which is one of the coolest things that ever happened since I realized I was cooler than Maverick.

I’m not gonna go through custom find types, you can find more info about them at Matt’s blog, or at this article written by someone whose name I think I’ve heard somewhere. What I’m going to talk about is how to mix your custom find types with pagination, without having to use paginate and paginateCount in your models.

So let’s first start by building yet another posts table, and inserting some records:

CREATE TABLE `posts`(
	`id` INT NOT NULL AUTO_INCREMENT,
	`title` VARCHAR(255) NOT NULL,
	`body` TEXT NOT NULL,
	`published` TINYINT(1) NOT NULL default 0,
	`created` DATETIME,
	`modified` DATETIME,
	PRIMARY KEY(`id`)
);

INSERT INTO `posts`(`title`, `body`, `published`, `created`, `modified`) VALUES
	('Post 1', 'Body for Post 1', 1, NOW(), NOW()),
	('Post 2', 'Body for Post 2', 0, NOW(), NOW()),
	('Post 3', 'Body for Post 3', 0, NOW(), NOW()),
	('Post 4', 'Body for Post 4', 1, NOW(), NOW()),
	('Post 5', 'Body for Post 5', 1, NOW(), NOW()),
	('Post 6', 'Body for Post 6', 0, NOW(), NOW()),
	('Post 7', 'Body for Post 7', 1, NOW(), NOW()),
	('Post 8', 'Body for Post 8', 1, NOW(), NOW()),
	('Post 9', 'Body for Post 9', 1, NOW(), NOW());

Now let’s assume we want a find type called published to fetch only the published posts, and that we also want to be able to paginate using this find type. We will be approaching this through a generic approach, something that can be used throughout all our models. With this in mind, let’s first introduce a model based member variable called $_types, where we define the specific needs of each custom find type. Therefore, that variable will hold what we need as conditions, order, etc. for each custom find type. So let’s build our Post model:

<?php
class Post extends AppModel {
	public $name = 'Post';
	protected $_types = array(
		'published' => array(
			'conditions' => array('Post.published' => 1),
			'order' => array('Post.created' => 'desc')
		)
	);
}
?>

As you can see, we define options for each find type as if we would be calling find() directly. So with the above, instead of doing:

$posts = $this->Post->find('all', array(
	'conditions' => array('Post.published' => 1),
	'order' => array('Post.created' => 'desc')
));

We can now do:

$posts = $this->Post->find('published');

Now, what if we wanted to paginate with the above custom find type? Just as we set pagination parameters through the controller member variable $paginate, we can specify which find type pagination we’ll use. We do so by specifying the find type in the index 0 of the pagination settings. Like so:

$this->paginate['Post'] = array(
	'published',
	'limit' => 10
);

$posts = $this->paginate('Post');

Easy, huh? When this is specified, paginate() does the following:

  1. It issues a find('count') on the Post model, specifying the custom find type (published) in the $options array, through an option named type. Therefore, we can use $options['type'] when our model is about to do the count to use the given options for our custom find type.
  2. It fetches the records by calling find() with the custom find type, find('published') in our example.

So where’s that sexy code? Add the following in your AppModel, making the above available for all our models.

<?php
class AppModel extends Model {
	public function find($type, $options = array()) {
		if (!empty($this->_types)) {
			$types = array_keys($this->_types);
			$type = (is_string($type) ? $type : null);
			if (!empty($type)) {
				if (($type == 'count' && !empty($options['type']) && in_array($options['type'], $types)) || in_array($type, $types)) {
					$options = Set::merge(
						$this->_types[($type == 'count' ? $options['type'] : $type)],
						array_diff_key($options, array('type'=>true))
					);
				}
				if (in_array($type, $types)) {
					$type = (!empty($this->_types[$type]['type']) ? $this->_types[$type]['type'] : 'all');
				}
			}
		}
		return parent::find($type, $options);
	}
}
?>

Now ain’t CakePHP great? Don’t tell me, tell everyone at CakeFest #3.



Mar232009

CakeFest #3: CakePHP in Berlin, July 9-12

What better place to talk CakePHP than the world’s beer nation? That was the question that made the CakePHP Team decide CakeFest third edition should be located in Berlin, Germany. Just as Buenos Aires was the meat fest, I hereby predict that this will be the Beer Fest.

Let’s face it, your code gets better when you use CakePHP. So attending the official CakePHP gathering where all Core Developers and prominent community members go to share their knowledge is an offer you should not ignore. Add awesome beer and guaranteed fun to that recipe, and you would be insane not to join us.

So what are you waiting for? Go and get your CakeFest ticket as soon as possible, you don’t want to be left behind! If you have a company, or you are a regular Baker just wanting to show your love back to the project, do not hesitate to sign up for the sponsorship packages. Also help us spread the word by placing these CakeFest badges in your site, and you may even get a free ticket!



 
Powered by Wordpress and MySQL. Clauz's design for by Cricava