CakePHP Tutorial No. 2 From IBM
CakePHP Tutorial No. 2 From IBM
12 Dec 2006
Prerequisites
It is assumed that you are familiar with the PHP programming language, have a
fundamental grasp of database design, and are comfortable getting your hands dirty.
A full grasp of the MVC design pattern is not necessary, as the fundamentals will be
covered during this tutorial. More than anything, you should be eager to learn, ready
to jump in, and anxious to speed up your development time.
System requirements
Before you begin, you need to have an environment in which you can work.
CakePHP has reasonably minimal server requirements:
2. PHP V4.3.2 or later (including PHP V5). This tutorial was written using
PHP V5.0.4
You'll also need a database ready for your application to use. The tutorial will
provide syntax for creating any necessary tables in MySQL.
The simplest way to download CakePHP is to visit CakeForge.org and download the
latest stable version. This tutorial was written using V1.1.8. (Nightly builds and
copies straight from Subversion are also available. Details are in the CakePHP
Manual (see Resources).)
<?php
if ($error)
{
e('Invalid Login.');
}
?>
Please log in.
</p>
<?php echo $html->form('/users/login') ?>
<label>Username:</label>
<?php echo $html->input('User/username', array) ?>
<label>Password:</label>
<?php echo $html->password('User/password', array) ?>
<?php echo $html->submit('login') ?>
</form>
<?php echo $html->link('register', '/users/register') ?>
Both of the views should look pretty straightforward. The index view just checks the
session for the user's username and, if it's not set, sends him to log in. The login
view doesn't set a specific error message, so someone trying to guess their way into
the system doesn't know which parts are correct.
Listing 3. Controller
<?php
class UsersController extends AppController
{
function register()
{
$this->set('username_error', 'Username must be between 6 and 40 characters.');
if (!empty($this->data))
{
if ($this->User->validates($this->data))
{
if ($this->User->findByUsername($this->data['User']['username']))
{
$this->User->invalidate('username');
$this->set('username_error', 'User already exists.');
} else {
$this->data['User']['password'] = md5($this->data['User']['password']);
$this->User->save($this->data);
$this->Session->write('user', $this->data['User']['username']);
$this->redirect('/users/index');
}
} else {
$this->validateErrors($this->User);
}
}
}
function knownusers()
{
$this->set('knownusers', $this->User->findAll(null, array('id', 'username',
'first_name', 'last_name', 'last_login'), 'id DESC'));
}
function login()
{
$this->set('error', false);
if ($this->data)
{
$results = $this->User->findByUsername($this->data['User']['username']);
if ($results && $results['User']['password'] ==
md5($this->data['User']['password']))
{
$this->Session->write('user', $this->data['User']['username']);
$this->Session->write('last_login', $results['User']['last_login']);
$results['User']['last_login'] = date("Y-m-d H:i:s");
$this->User->save($results);
$this->redirect('/users/index');
} else {
$this->set('error', true);
}
}
}
function logout()
{
$this->Session->delete('user');
$this->redirect('/users/login');
}
function index()
{
$username = $this->Session->read('user');
if ($username)
{
$results = $this->User->findByUsername($username);
$this->set('User', $results['User']);
$this->set('last_login', $this->Session->read('last_login'));
} else {
$this->redirect('/users/login');
}
}
}
?>
The use of md5() to hash passwords and compare their hashed values means you
don't have to store plaintext passwords in the database -- as long as you hash the
passwords before you store them. The logout action doesn't need a view; it just
needs to clear the values you put into session.
It's OK if your solutions don't look exactly like these. If you didn't get to your own
solutions, update your code using the above so that you will be ready to complete
the rest of this tutorial.
Section 3. Scaffolding
Right now, Tor doesn't do a whole lot. It lets people register, log in, and see who
else is registered. Now what it needs is the ability for users to enter some products
into the catalog or view some products from other users. A good way to get a
jumpstart on this is to use scaffolding.
Scaffolding is a concept that comes from Ruby on Rails (see Resources). It's an
excellent way to get some database structure built quickly to prototype the
application, without writing a bunch of throwaway code. But scaffolding, as the name
implies, is something that should be used to help build an application, not something
to build an application around. Once the application logic starts to get complicated,
the scaffolding may need to get replaced by something more solid.
Additionally, it will be helpful for this demonstration to insert some data into the
dealers table.
An important note about scaffolding: Remember that note from setting up the
database about foreign keys following the format singular_id like user_id or
winner_id? In CakePHP, scaffold will expect that any field ending in _id is a
foreign key to a table with the name of whatever precedes the _id -- for example,
scaffolding will expect that dealer_id is a foreign key to the table dealers.
<?php
class Product extends AppModel
{
var $name = 'Product';
var $belongsTo = array ('Dealer' => array(
'className' => 'Dealer',
'conditions'=>,
'order'=>,
'foreignKey'=>'dealer_id')
);
}
?>
Model associations
Model associations tell a model that it relates in some way to another model. Setting
up proper associations between your models will allow you to deal with entities and
their associated models as a whole, rather than individually. In CakePHP, there are
four types of model associations:
hasOne
The hasOne association tells the model that each entity in the model has one
corresponding entity in another model. An example of this would be a user
entity's corresponding profile entity (assuming a user is only permitted one
profile).
hasMany
The hasMany association tells the model that each entity in the model has
several corresponding entities in another model. An example of this would be a
category model having many things that belong to the category (posts,
products, etc.). In the case of Tor, a dealer entity has many products.
belongsTo
This tells a model that each entity in the model points to an entity in another
model. This is the opposite of hasOne, so an example would be a profile entity
pointing back to one corresponding user entity.
hasAndBelongsToMany
This association indicates that an entity has many corresponding entities in
another model and also points back to many corresponding entities in another
model. An example of this might be a recipe. Many people might like the
recipe, and the recipe would have several ingredients.
The belongsTo variable in this case indicates that each product in the products
table "belongs to" a particular dealer.
<?php
class Dealer extends AppModel
{
var $name = 'Dealer';
var $hasMany = array ('Product' => array(
'className' => 'Product',
'conditions'=>,
'order'=>,
'foreignKey'=>'dealer_id')
);
}
?>
You can skip adding data validation for now, but as the application evolves, you may
get ideas for different types validation to add.
<?php
class ProductsController extends AppController
{
var $scaffold;
}
?>
Save the controller, then visit http://localhost/products (yes -- without creating any
views, or a Dealer controller). You should see something like Figure 1.
It's really that simple. Just like that, you have an interface into your products table
that lets you add, edit, delete, list, slice, and julienne your products.
Try adding a product. You should be prompted to enter a title and a description for
the product, as well as select a dealer. Did that list of dealers look familiar? It should
have; you inserted them into the dealer table just after you created it. Scaffolding
recognized the table associations as you defined them, and auto-generated that
drop-down dealer list for you.
Now go back and look at the amount of code you wrote to get all of this functionality.
Before you proceed, make a copy of your existing app directory. Bake will overwrite
the products controller, and you should always back up your files when an operation
involves the word "overwrite" (or "copy," "delete," "format," or "voodoo"). You may
want to up the value of max_input_time in the php.ini file when using Bake,
especially in the beginning. If you take too long entering your information, Bake will
time out.
If you have problems getting this to run, make sure that php is in your path -- if it's
not, specifying the full path to your PHP executable should be sufficient.
For the Tor application, the model you've written should be fine, so let's start with the
controller. Press C to select controller. You will be prompted to enter the name of the
controller.
Keep in mind that in CakePHP, controller names are plural. Enter the name
products and press Enter to continue. Bake will ask if you want to build the
controller interactively. For now, press N to let Bake make all the decisions on its
own, but later on, you should try building a controller interactively to get a feel for
what Bake can do for you beyond this.
<?php
class ProductsController extends AppController
{
//var $scaffold;
var $name = 'Products';
var $helpers = array('Html', 'Form' );
function index() {
$this->Product->recursive = 0;
$this->set('products', $this->Product->findAll());
}
function add() {
if(empty($this->data)) {
$this->render();
} else {
$this->cleanUpFields();
if($this->Product->save($this->data)) {
$this->Session->setFlash('The Product has been saved');
$this->redirect('/products/index');
} else {
$this->Session->setFlash('Please correct errors below.');
}
}
}
function edit($id) {
if(empty($this->data)) {
$this->data = $this->Product->read(null, $id);
} else {
$this->cleanUpFields();
if($this->Product->save($this->data)) {
$this->Session->setFlash('The Product has been saved');
$this->redirect('/products/index');
} else {
$this->Session->setFlash('Please correct errors below.');
}
}
}
function view($id) {
$this->set('product', $this->Product->read(null, $id));
}
function delete($id) {
if($this->Product->del($id)) {
$this->Session->setFlash('The Product deleted: id '.$id.);
$this->redirect('/products/index');
}
}
}
?>
This controller Bake has generated contains the same basic functionality you got
from including scaffolding, but now you can tweak it to fit your needs. Pretty slick.
Ready to go again?
The initial Bake menu should look just like it did when you baked the controller. This
time, though, it's time to bake some views. Press V to select views. You will be
prompted to enter the name of the controller for which you are baking views. As
before, enter the name products and press Enter to continue. Bake will ask if you
want to build the views interactively. For now, press N to let Bake make all the
decisions on its own. You can come back later and play around with interactive
baking.
Open the index and take have a look. It should look something like Listing 9.
<h2>List Products</h2>
<table cellpadding="0" cellspacing="0">
<tr>
<th>Id</th>
<th>Title</th>
<th>Dealer Id</th>
<th>Description</th>
<th>Actions</th>
</tr>
<?php foreach ($products as $product): ?>
<tr>
<td><?php echo $product['Product']['id'] ?></td>
<td><?php echo $product['Product']['title'] ?></td>
<td><?php echo $product['Product']['dealer_id'] ?></td>
<td><?php echo $product['Product']['description'] ?></td>
<td>
<?php echo $html->link('View','/products/view/' . $product['Product']['id'])?>
<?php echo $html->link('Edit','/products/edit/' . $product['Product']['id'])?>
<?php echo $html->link('Delete','/products/delete/' . $product['Product']['id'],
null, 'Are you sure you want to delete: id ' . $product['Product']['id'])?>
</td>
</tr>
<?php endforeach; ?>
</table>
<ul class="actions">
<li><?php echo $html->link('New Product', '/products/add'); ?></li>
</ul>
Take a look in the those other views, as well. That's a whole lot of writing you didn't
have to do. You'll be tweaking these views later in this tutorial to help lock down Tor.
You've baked a controller and the necessary views for the products functionality.
Take it for a spin. Start at http://localhost/products and walk through the
various parts of the application. Add a product. Edit one. Delete another. View a
product. It should look exactly like it did when you were using scaffolding.
What is an ACL?
An ACL is, in essence, a list of permissions. That's all it is. It is not a means for user
authentication. It's not the silver bullet for PHP security. An ACL is just a list of who
can do what.
The who is usually a user (but it could be something like a controller). The who is
referred to as an access request object (ARO). The do what in this case is going to
typically mean "execute some code". The do what is referred to as an access control
object (ACO).
Therefore, an ACL is a list of AROs and the ACOs they have access to. Simple,
right? It should be. But it's not.
As soon as the explanation departed from "it's a list of who can do what" and started
throwing all those three-letter acronyms (TLAs) at you, things may have gone
Imagine there's a party going on at some nightclub. Everyone who's anyone is there.
The party is broken up into several sections -- there's a VIP lounge, a dance floor,
and a main bar. And of course, a big line of people trying to get in. The big scary
bouncer at the door checks a patron's ID, looks at the List, and either turns the
patron away or lets him come into the section of the party to which he has been
invited.
Those patrons are AROs. They are requesting access to the different sections of the
party. The VIP lounge, dance floor, and main bar are all ACOs. The ACL is what the
big scary bouncer at the door has on his clipboard. The big scary bouncer is
CakePHP.
That's all it takes to get started. Now it's time to start defining your AROs and your
ACOs.
It makes the most sense to add this to the registration portion of the application. That
way, when new users sign up, their corresponding ARO is automatically created for
them. This does mean you'll have to manually create a couple AROs for the users
you've already created, but CakePHP makes that easy, as well.
Defining groups
In CakePHP (and when using ACLs, in general), users can be assigned to groups
for the purpose of assigning or revoking permissions. This greatly simplifies the task
of permission management, as you do not need to deal with individual user
permissions, which can grow into quite a task if your application has more then a few
users.
For Tor, you're going to define two groups. The first group, called users, will be used
to classify everyone who has simply registered for an account. The second group,
called dealers, will be used to grant certain users additional permissions within Tor.
You will create both of these groups using the CakePHP command-line acl.php
script, much like you did to create the ACL database. To create the groups, execute
the commands below from the cake/scripts directory.
After each command, CakePHP should display a message saying the ARO was
created.
The parameters you passed in (for example, '0 null Users') are link_id,
parent_id, and alias. The link_id parameter is a number that would normally
correspond to the database ID of, for example, a user. Since these groups will never
correspond to a database record, you passed in 0. The parent_id parameter
would correspond to a group to which the ARO should belong. As these groups are
top-level, you passed in null. The alias parameter is a string used to refer to the
group.
function register()
{
$this->set('username_error', 'Username must be between 6 and 40 characters.');
if (!empty($this->data))
{
if ($this->User->validates($this->data))
{
if ($this->User->findByUsername($this->data['User']['username']))
{
$this->User->invalidate('username');
$this->set('username_error', 'User already exists.');
} else {
$this->data['User']['password'] = md5($this->data['User']['password']);
$this->User->save($this->data);
$this->Session->write('user', $this->data['User']['username']);
$this->redirect('/users/index');
}
} else {
$this->validateErrors($this->User);
}
}
}
To start using CakePHP's ACL component, you will need to include the component
as a class variable.
<?php
class UsersController extends AppController
{
var $components = array('Acl');
...
Now you have access to all the functionality provided by the ACL component.
Creating an ARO for your user is as simple as creating an ARO object and invoking
the create method. This method takes the same three parameters you passed at
the command line to create the groups: link_id, parent_id, and alias. To
create the ARO for your user, you also need to know what the user's ID is once it
has been saved. You can get this from $this->User->id after the data has been
saved.
Putting it all together, your register function now might look something like Listing
12.
function register()
{
$this->set('username_error', 'Username must be between 6 and 40 characters.');
if (!empty($this->data))
{
if ($this->User->validates($this->data))
{
if ($this->User->findByUsername($this->data['User']['username']))
{
$this->User->invalidate('username');
$this->set('username_error', 'User already exists.');
} else {
$this->data['User']['password'] = md5($this->data['User']['password']);
if ($this->User->save($this->data))
{
$aro = new Aro();
$aro->create($this->User->id, 'Users',
$this->data['User']['username']);
$this->Session->write('user', $this->data['User']['username']);
$this->redirect('/users/index');
} else {
$this->flash('There was a problem saving this information', '/users/register');
}
}
} else {
$this->validateErrors($this->User);
}
}
}
You'll note that this register function includes a check to verify that the user data
was saved successfully before proceeding with creation of the ARO.
Try it out
That should be all you need to get your AROs up and running. To verify, start back
at the command line in cake/scripts and use the acl.php script to view the ARO tree:
php acl.php view aro. Your output should look something like Figure 7.
From now on, whenever someone registers for a new account, he will automatically
have an ARO created for him. That ARO will belong to the users group.
Don't be confused by the numbers preceding the groups and users in the ARO list.
That number is not the link_id for the ARO you've created.
Then, for each user, you need to execute the create aro command like you did for
creating the groups. For link_id, specify the ID of the user. For parent_id, null.
You will need to set the parent with a separate command. For alias, specify the
username. For example, from Figure 9, to create an ARO for Tor Johnson, you
would execute the following (again, from the cake/scripts directory): php acl.php
create aro 1 null wrestler. And to set the parent for Tor, execute the
setParent command, passing in the ID of the ARO and the parent ID: php
acl.php setParent aro wrestler Users.
Make sure you run these commands for each user in your knownusers list, except
for the user you created to test ARO creation during user registration. Be sure that
you are specifying the right user ID and username for each user. When you're done,
the results of php acl.php view aro should look something like Figure 10.
You can get help on some of the other things that acl.php can do by running php
acl.php help at the command line. The acl.php script is a helpful tool, but does
not appear to be complete as of CakePHP V1.1.8.3544.
function add() {
if(empty($this->data)) {
$this->set('dealerArray', $this->Product->Dealer->generateList());
$this->render();
} else {
$this->cleanUpFields();
if($this->Product->save($this->data)) {
$this->Session->setFlash('The Product has been saved');
$this->redirect('/products/index');
} else {
$this->Session->setFlash('Please correct errors below.');
$this->set('dealerArray', $this->Product->Dealer->generateList());
}
}
}
</code>
Once again, CakePHP makes adding the definition for your ACOs very simple. You
start by adding the $components class variable to the controller, like you did for the
users controller.
<?php
class ProductsController extends AppController
{
var $components = array('Acl');
...
Creating an ACO looks almost exactly like creating an ARO. You create an ACO
object and call the create method, passing in a link_id (in this case, a product
ID), a parent_id (you will create groups representing your dealers here), and an
alias (in this case, the product title by itself probably isn't a good idea because it's
not unique; instead, you will amend the product id to the beginning of the product
title). Putting these pieces into your add function, it should look something like
Listing 15.
function add() {
if(empty($this->data)) {
$this->set('dealerArray', $this->Product->Dealer->generateList());
$this->render();
} else {
$this->cleanUpFields();
if($this->Product->save($this->data)) {
$aco = new Aco();
$product_id = $this->Product->getLastInsertID();
$aco->create($product_id, $this->data['Product']['dealer_id'],
$product_id.'-'.$this->data['Product']['title']);
$this->Session->setFlash('The Product has been saved');
$this->redirect('/products/index');
} else {
$this->Session->setFlash('Please correct errors below.');
$this->set('dealerArray', $this->Product->Dealer->generateList());
}
}
}
That should be all you need to auto-create the ACOs for the products created in Tor.
Before you continue, you should create ACOs for the existing products and groups.
Once again, from the command line, in the cake/scripts directory, you will run some
create commands.
Start by creating groups to represent the dealers you created way back when you
created the dealer table. But this time, specify that you are creating an ACO and
provide the dealer ID.
You can run php acl.php view aco to verify that the groups look as expected.
Next, delete the existing products from the products table. You should be able to do
this by going to the products index (http://localhost/products/index) and clicking
Delete next to each product.
There are a couple reasons to delete these products before continuing. A defect in
acl.php at V1.1.8.3544 makes granting permissions at the command line
problematic. You could write or modify an action to grant permissions on existing
products, rather than deleting existing products. But because you have only created
a couple of products thus far, deleting them is the easiest solution.
Don't test out that new product add function just yet. Now that you have ACOs
created for your existing dealers and you've deleted the existing products, you're
ready to proceed with setting up some permissions.
representing products, grouped by dealer. It's time to glue them together by defining
some permissions.
Defining policies
Defining permission policies is more than just writing and executing code. You need
to think about what your ACL is actually trying to accomplish. Without a clear picture
of what you are trying to protect from whom, you will find yourself constantly
redefining your permissions.
Tor has users and products. For the purpose of this tutorial, you are going allow the
user who created the product full permissions to edit and delete the product. Any
user will be able to view the product unless explicitly denied access.
If you do not specify a TYPE (create, read, update, or delete), CakePHP will
assume you are granting full permission. Your new add function in the products
controller should look like this:
function add() {
if(empty($this->data)) {
$this->set('dealerArray', $this->Product->Dealer->generateList());
$this->render();
} else {
$dealer_id = $this->data['Product']['dealer_id'];
$product_alias = '-'.$this->data['Product']['title'];
$this->cleanUpFields();
if($this->Product->save($this->data)) {
$product_id = $this->Product->id;
$aco = new Aco();
$product_alias = $product_id.$product_alias;
$aco->create ($product_id, $dealer_id, $product_alias);
$this->Acl->allow('Users', $product_alias,'read');
$this->Acl->allow($this->Session->read('user'), $product_alias, '*');
$this->Session->setFlash('The Product has been saved');
$this->redirect('/products/index');
} else {
$this->Session->setFlash('Please correct errors below.');
$this->set('dealerArray', $this->Product->Dealer->generateList());
}
}
}
Try logging in as wrestler and adding a couple of products, just to see that nothing
got broken along the way. You're almost done. You've defined your AROs, your
ACOs, and you have assigned permissions. Now Tor needs check permissions
when performing the various product-related actions.
You are going to add a couple lines to each action in the products controller. These
lines will check the user for access and permit or deny the action based on the
permissions.
function view($id) {
$product = $this->Product->read(null, $id);
if ($this->Acl->check($this->Session->read('user'), $id.'-'.$product['title'],
'read'))
{
$this->set('product', $product);
} else {
$this->Session->setFlash('Only Registered Users may view this product.');
$this->redirect('/users/register');
}
}
Save the file, make sure you are logged out of Tor and visit the products list at
http://localhost/products. When you click on any of the products, you should get
redirected to the User Registration page.
Now log in using any account and try it again. This time you should be able to view
the product.
That tackles the first part of the permissions. Now you need to tell Tor to deny edit
and delete access to anyone but the user who created the product.
function edit($id) {
$product = $this->Product->read(null, $id);
if ($this->Acl->check($this->Session->read('user'),
$id.'-'.$product['Product']['title'],
'update'))
{
if(empty($this->data)) {
$this->data = $this->Product->read(null, $id);
$this->set('dealerArray', $this->Product->Dealer->generateList());
} else {
$this->cleanUpFields();
if($this->Product->save($this->data)) {
$this->Session->setFlash('The Product has been saved');
$this->redirect('/products/index');
} else {
For the delete controller, you should add an additional line -- one to delete the ACO
for the product. Your delete action will look like Listing 19.
function delete($id) {
$product = $this->Product->read(null, $id);
if ($this->Acl->check($this->Session->read('user'),
$id.'-'.$product['Product']['title'], 'delete'))
{
if($this->Product->del($id)) {
$aco = new Aco();
$aco->delete($id.'-'.$product['Product']['title']);
$this->Session->setFlash('The Product deleted: id '.$id.);
$this->redirect('/products/index');
}
} else {
$this->Session->setFlash('You cannot delete this product.');
$this->redirect('/products/index');
}
}
You don't have to go through and revoke the corresponding permissions because
CakePHP does it for you.
Save the products controller and try it out. Start by logging out at
http://localhost/users/logout, then go back to your products list at
http://localhost/products/ and try to edit or delete a product. You should get directed
back to the products list with a message.
Now log in as the user wrestler and try to edit a product. Then delete it. You should
have no trouble.
Log out again at http://localhost/users/logout and log in as the user future and try to
edit or delete another product, and you will find you are unable to. While you're here,
create a new product, then try to modify or delete the product as another user.
Dealers
As you may have noticed from the products views that Bake built, there are links in
the index view that point to dealers. Like you did with products, use Bake to build a
controller and views for dealers. Don't build a model, as you already have one
Modify the dealer's add action to verify that the dealer name is unique.
ACLs
There's a bug in the add action for the products controller. It doesn't check to see
who can create a product. This functionality should only be available to users. Fix
the bug.
Once you have dealers built, using the ACL skills you have learned, protect all
dealer functionality from anyone not belonging to the dealers group.
Once that is complete, using ACLs, allow any user to create a dealer. You will note
that the ACOs that are created for products go into ACO groups representing the
dealers. How would you set up ACLs so that any member of the dealership could
change a product, but only the product creator could delete the product?
Views
In the products index view, come up with a way to only display Edit and Delete
buttons for products the user can edit or delete.
Part 3 shows how to use Sanitize, a handy CakePHP class, which helps secure an
application by cleaning up user-submitted data.
Downloads
Description Name Size Download method
Part 2 source code os-php-cake2.source.zip
6KB HTTP
Resources
Learn
• Visit CakePHP.org to learn more about it.
• The CakePHP API has been thoroughly documented. This is the place to get
the most up-to-date documentation for CakePHP.
• There's a ton of information available at The Bakery, the CakePHP user
community.
• CakePHP Data Validation uses PHP Perl-compatible regular expressions.
• Read a tutorial titled "How to use regular expressions in PHP."
• Want to learn more about design patterns? Check out Design Patterns:
Elements of Reusable Object-Oriented Software , also known as the "Gang Of
Four" book.
• Check out some Source material for creating users.
• Check out the Wikipedia Model-View-Controller.
• Here is more useful background on the Model-View-Controller.
• Here's a whole list of different types of software design patterns.
• Read about Design Patterns.
• Visit IBM developerWorks' PHP project resources to learn more about PHP.
• Stay current with developerWorks technical events and webcasts.
• Check out upcoming conferences, trade shows, webcasts, and other Events
around the world that are of interest to IBM open source developers.
• Visit the developerWorks Open source zone for extensive how-to information,
tools, and project updates to help you develop with open source technologies
and use them with IBM's products.
• To listen to interesting interviews and discussions for software developers, be
sure to check out developerWorks podcasts.
Get products and technologies
• Innovate your next open source development project with IBM trial software,
available for download or on DVD.
Discuss
• The developerWorks PHP Developer Forum provides a place for all PHP
developer discussion topics. Post your questions about PHP scripts, functions,
syntax, variables, PHP debugging and any other topic of relevance to PHP
developers.
• Get involved in the developerWorks community by participating in
developerWorks blogs.