Приглашаем посетить
Чернышевский (chernyshevskiy.lit-info.ru)

Implementing Classes

Previous
Table of Contents
Next

Implementing Classes

By far the largest change from PHP 4 to PHP 5 is the new object model. Mirroring this, the biggest change from PHP 4 extensions to PHP 5 extensions is handling classes and objects. The procedural extension code you learned in Chapter 21, "Extending PHP: Part I," is almost entirely backward-portable to PHP 4. The use of macros for many of the functions helps things: Macros allow for internal reimplementation without invalidating extension code. Class code, however, is substantially different in PHP 5 than in PHP 4. Not only have internal Zend Engine structures changed, but the basic semantics of classes have changed as well. This means that although certain parts of writing classes remain the same, many are completely different.

To create a new class, you must first create and register its zend_class_entry data type. A zend_class_entry struct looks like this:

struct _zend_class_entry {
    char type;
    char *name;
    zend_uint name_length;
    struct _zend_class_entry *parent;
    int refcount;
    zend_bool constants_updated;
    zend_uint ce_flags;

    HashTable function_table;
    HashTable default_properties;
    HashTable properties_info;
    HashTable *static_members;
    HashTable constants_table;
    struct _zend_function_entry *builtin_functions;

    union _zend_function *constructor;
    union _zend_function *destructor;
    union _zend_function *clone;
    union _zend_function *_ _get;
    union _zend_function *_ _set;
    union _zend_function *_ _call;

    zend_class_iterator_funcs iterator_funcs;

    /* handlers */
    zend_object_value (*create_object)(zend_class_entry *class_type TSRMLS_DC);
    zend_object_iterator *(*get_iterator)
           (zend_class_entry *ce, zval *object TSRMLS_DC);
    int (*interface_gets_implemented)
           (zend_class_entry *iface, zend_class_entry *class_type TSRMLS_DC);
    zend_class_entry **interfaces;
    zend_uint num_interfaces;

    char *filename;
    zend_uint line_start;
    zend_uint line_end;
    char *doc_comment;
    zend_uint doc_comment_len;
};

This is not small. Fortunately, there are macros to help you with most of it. Note the following:

  • The struct contains hashtables for all methods, constants, static properties, and default property values.

  • Although it has a private hashtable for methods, it has separate zend_function slots for its constructor, destructor, clone, and overload handlers.

Creating a New Class

To create an empty class like this:

class Empty {}

requires only a few steps. First, in the main scope of the extension, you declare a zend_class_entry pointer that you will register your class into:

static zend_class_entry *empty_ce_ptr;

Then, in your MINIT handler, you use the INIT_CLASS_ENTRY() macro to initialize the class and the zend_register_internal_class() function to complete the registration:

PHP_MINIT_FUNCTION(cart)
{
  zend_class_entry empty_ce;
  INIT_CLASS_ENTRY(empty_ce, "Empty", NULL);
  empty_ce_ptr = zend_register_internal_class(&empty_ce);
}

empty_ce is used here as a placeholder to initialize class data before handing it off to zend_register_internal_function(), which handles the registration of the class into the global class table, initialization of properties and constructors, and so on. INIT_CLASS_ENTRY() takes the placeholder zend_class_entry (which, as you saw in Chapter 21, is a nontrivial data structure), and initializes all its attributes to standard default values. The second parameter to INIT_CLASS_ENTRY() is the name of the class being registered. The third parameter to INIT_CLASS_ENTRY(), which is being passed here as NULL, is the method table for the class.

empty_ce_ptr is useful because it is a live pointer to the class entry for the class that is sitting in the global function table. Normally to access a class, you would need to look it up by name in this global hashtable. By keeping a static pointer to it in the extension, you can save yourself that lookup.

When you use zend_register_internal_class(), the engine knows that the class is supposed to be persistent, meaning that like functions, they will only be loaded into the global class table once, when the server starts.

Of course, a class without any properties or methods is neither very interesting nor very useful. The first thing you need to add to a class is properties.

Adding Properties to a Class

Instance properties in PHP classes are either dynamic properties (belonging only to a particular object) or default properties (belonging to the class). Default instance properties are not static properties. Every instance has its own copy of default class properties, but every instance is guaranteed to have a copy. Dynamic instance properties are properties that are not declared in a class definition but are instead created on-the-fly after an object has been created.

Dynamic instance variables are commonly defined in a class's constructor, like this:

class example {
  public function _ _constructor()
  {
    $this->instanceProp = 'default';
  }
}

PHP 5 allows for dynamic creation of instance variables such as these, but this type of variable creation is largely for backward compatibility with PHP 4. There are two major problems with dynamic instance properties:

  • Because they are not part of the class entry, they cannot be inherited.

  • Because they are not part of the class entry, they are not visible through the reflection API.

The preferred PHP 5 method is to declare the variable in the class definition, like this:

class example {
  public $instanceProp = 'default';
}

In PHP 4 it is standard to create all extension class properties as dynamic instance properties, usually in the class constructor. In PHP 5, extension classes should look more like PHP classes (at least in their public interface). This means you need to be able to create an extension class HasProperties that looks like the following.

class HasProperties {
  public $public_property = 'default';
  public $unitialized_property;
  protected $protected_property;
  private $private_property;
}

Furthermore, this class should behave as a regular PHP class when it comes to inheritance and PPP. Of course, there is a set of helper functions for handling all this:

zend_declare_property(zend_class_entry *ce, char *name, int name_length,
                       zval *property, int access_type TSRMLS_DC);
zend_declare_property_null(zend_class_entry *ce, char *name, int name_length,
                            int access_type TSRMLS_DC);
zend_declare_property_long(zend_class_entry *ce, char *name, int name_length,
                            long value, int access_type TSRMLS_DC);
zend_declare_property_string(zend_class_entry *ce, char *name, int name_length,
                              char *value, int access_type TSRMLS_DC);

ce is the class you are registering the property into. name is the name of the property you are registering. name_length is the length of name.access_type is a flag that determines the access properties for the property. The following are the property setting mask bits:

   mask
   ZEND_ACC_STATIC
   ZEND_ACC_ABSTRACT
   ZEND_ACC_FINAL
   ZEND_ACC_INTERFACE
   ZEND_ACC_PUBLIC
   ZEND_ACC_PROTECTED
   ZEND_ACC_PRIVATE

To use a property declaration function, you call it immediately after class registration. The following is a C implementation of HasProperties:

Note

Note that for clarity I've separated the class registration code into a helper function that is called from PHP_MINIT_FUNCTION(). Cleanliness and compartmentalization are essential to code maintainability.


static zend_class_entry *has_props_ptr;

void register_HasProperties(TSRMLS_D)
{
  zend_class_entry ce;
  zval *tmp;

  INIT_CLASS_ENTRY(ce, "HasProperties", NULL);
  has_props_ptr = zend_register_internal_class(&ce TSRMLS_CC);

  zend_declare_property_string(has_props_ptr,
                          "public_property", strlen("public_property"),
                          "default", ACC_PUBLIC);

  zend_declare_property_null(has_props_ptr,

  zend_declare_property_null(has_props_ptr, "uninitialized_property",
                         strlen("uninitialized_property"), ACC_PUBLIC);

  zend_declare_property_null(has_props_ptr, "protected_property",
                         strlen("protected_property"), ACC_PROTECTED);

  zend_declare_property_null(has_props_ptr, "private_property",
                         strlen("private_property"), ACC_PRIVATE);
}

PHP_MINIT_FUNCTION(example)
{
  register_HasProperties(TSRMLS_CC);
}

Class Inheritance

To register a class as inheriting from another class, you should use the following function:

zend_class_entry *zend_register_internal_class_ex(zend_class_entry *class_entry,
                                            zend_class_entry *parent_ce,
                                            char *parent_name TSRMLS_DC);

class_entry is the class you are registering. The class you are inheriting from is specified by passing either a pointer to its zend_class_entry structure (parent_ce) or by passing the parent class's name, parent_name. For example, if you want to create a class ExampleException that extends Exception, you could use the following code:

static zend_class_entry *example_exception_ptr;

void register_ExampleException(TSRMLS_DC)
{
  zend_class_entry *ee_ce;
  zend_class_entry *exception_ce = zend_exception_get_default();
  INIT_CLASS_ENTRY(ee_ce, "ExampleException", NULL);
  example_exception_ptr =
     zend_register_internal_class_ex(ee_ce, exception_ce, NULL TSRMLS_CC);
}

PHP_MINIT_FUNCTION(example)
{
  register_ExampleException(TSEMLS_CC);
}

This code example is almost identical to the class registration example presented earlier in this chapter, in the section "Creating a New Class," with one critical difference. In this code, you pass a pointer to the Exception class zend_class_entry structure (obtained via zend_exception_get_default()) as the second parameter to zend_register_internal_class_ex(). Because you know the class entry, you do not need to pass in parent_name.

Private Properties

Although it may not yet be fully clear, defining private properties in classes is a bit silly. Because private properties cannot be accessed from outside the class or by derived classes, they really are purely for internal use. Therefore, it would make more sense to have your private variables be structs of native C types. You'll soon see how to accomplish this.


Adding Methods to a Class

After adding properties to a class, the next thing you most likely want to do is add methods. As you know from programming PHP, class methods are little more than functions. The little more part is that they have a class as their calling context, and (if they are not static methods) they have the object they are acting on passed into them. In extension code, the paradigm stays largely the same. Extension class methods are represented by a zend_function type internally and are declared with the ZEND_METHOD() macro.

To gain access to the calling object ($this), you use the function getThis(), which returns a zval pointer to the object handle.

To assist in finding properties internally, the Zend API provides the following accessor function:

zval *zend_read_property(zend_class_entry *scope, zval *object, char *name,
                      int name_length, zend_bool silent TSRMLS_DC);

This function looks up the property named name in object of class scope and returns its associated zval.silent specifies whether an undefined property warning should be emitted if the property does not exist.

The standard way of using this function is as follows:

zval *data, *obj;
obj = getThis();
data = zend_read_property(Z_OBJCE_P(obj), obj, "property",
                       strlen("property"), 1 TSRMLS_CC);

Although it is possible to access the property's hashtable directly via looking at Z_OBJPROP_P(obj), you almost never want to do this. zend_read_property() correctly handles inherited properties, automatic munging of private and protected properties, and custom accessor functions.

Similarly, you do not want to directly update an object's properties hashtable, but instead should use one of the zend_update_property() functions. The simplest update function is the following:

void zend_update_property(zend_class_entry *scope, zval *object, char *name,
                       int name_length, zval *value TSRMLS_DC);

This function updates the property name in the object object in class scope to be value. Like array values, there are convenience functions for setting property values from base C data types. Here is a list of these convenience functions:

void zend_update_property_null(zend_class_entry *scope, zval *object,
                                char *name, int name_length TSRMLS_DC);
void zend_update_property_long(zend_class_entry *scope, zval *object,
                                char *name, int name_length,
				long value TSRMLS_DC);
void zend_update_property_string(zend_class_entry *scope, zval *object,
                                  char *name, int name_length,
                                  char *value TSRMLS_DC);

These functions work identically to the zend_declare_property() functions presented in the previous section.

To see how this works, consider the following PHP code taken from the classic object-orientation example in the PHP manual:

class Cart {
  public $items;

  function num_items()
  {
    return count($this->items);
  }
}

Assuming that Cart has already been defined in the extension, num_items() would be written as follows:

PHP_FUNCTION(cart_numitems)
{
  zval *object;
  zval *items;
  HashTable *items_ht;

  object = getThis();
  items = zend_read_property(Z_OBJCE_P(object), object, "items",
			  strlen("items"), 1 TSRMLS_CC),
  if(items) {
    if(items_ht = HASH_OF(items)) {
      RETURN_LONG(zend_hash_num_elements(items_ht));
    }
  }
  RETURN_FALSE;
}

To register this in your class, you define a table of methods called cart_methods and then pass that into INIT_CLASS_ENTRY() when you initialize Cart:

static zend_class_entry *cart_ce_ptr;

static zend_function_entry cart_methods[] = {
  ZEND_ME(cart, numitems, NULL, ZEND_ACC_PUBLIC)

  {NULL, NULL, NULL}
};

void register_cart()
{
  zend_class_entry ce;
  INIT_CLASS_ENTRY(ce, "Cart", cart_methods);
  cart_ce_ptr = zend_register_internal_class(*ce TSRMLS_CC);
  zend_declare_property_null(has_props_ptr, "items",
                         strlen("items"), ACC_PUBLIC);
}

PHP_MINIT_FUNCTION(cart)
{
  register_cart();
}

Note that the zend_function_entry array looks a bit different than it did before. Instead of PHP_FE(cart_numitems, NULL), you have ZEND_ME(cart, numitems, NULL, ZEND_ACC_PUBLIC). This allows you to register the function defined by ZEND_METHOD(cart, numitems) as the public method numitems in the class cart. This is useful because it handles all the name munging necessary to avoid function naming conflicts while allowing the method and class names to appear clean.

Adding Constructors to a Class

Special cases for method names are the constructor, destructor, and clone functions. As in userspace PHP, these functions should be registered with the names _ _construct, _ _destruct, and _ _clone, respectively.

Other than this, there is nothing particularly special about a constructor, destructor, or clone function. is the following constructor for Cart allows an object to be passed in:

class Cart {
  public $items;

  public function _ _construct($item)
  {
    $this->items[] = $item;
  }
  /* ... */
}

In C, this constructor is as follows:

ZEND_METHOD(cart, _ _construct)
{
  zval *object;
  zval *items;
  zval *item;
  if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &item) == FAILURE) {
    return;
  }
  object = getThis();
  MAKE_STD_ZVAL(items);
  array_init(items);
  add_next_index_zval(items, item);
  zend_declare_property(Z_OBJCE_P(object), object, "items", strlen("items"),
                    items, ZEND_ACC_PUBLIC TSRMLS_CC);
}

To register this function, you only need to add it to the cart_methods array, as follows:

static zend_function_entry cart_methods[] = {
  ZEND_ME(cart, _ _construct, NULL, ZEND_ACC_PUBLIC),
  ZEND_ME(cart, numitems, NULL, ZEND_ACC_PUBLIC),

  {NULL, NULL, NULL}
};
PHP_MINIT(cart)
{
}

Throwing Exceptions

As part of a robust error-handling scheme, you need to be able to throw exceptions from extensions. There is considerable debate among PHP developers concerning whether throwing exceptions in extensions is a good idea. Most of the arguments are centered around whether it is okay to force developers into a certain coding paradigm. Most of the extensions you will write will be for your own internal use. Exceptions are an incredibly powerful tool, and if you are fond of using them in PHP code, you should not shy away from them in extension code.

Throwing an exception that derives from the base Exception class is extremely easy. The best way to do so is to use the following helper function:

void zend_throw_exception(zend_class_entry *exception_ce,
                         char *message, long code TSRMLS_DC);

To use this function, you supply a class via exception_ce, a message via message, and a code via code. The following code throws an Exception object:

zend_throw_exception(zend_exception_get_default(), "This is a test", 1 TSRMLS_CC);

There is also a convenience function to allow string formatting of the exception's message:

void zend_throw_exception_ex(zend_class_entry *exception_ce,
                         long code TSRMLS_DC, char *format, ...);

Note that code is now in the first position, while the message parameter to zend_throw_exception() is replaced with fmt and a variable number of parameters. Here is a single line of code to throw an exception that contains the file and line number of the C source file where the exception was created:

zend_throw_exception_ex(zend_exception_get_default(), 1,
                        "Exception at %s:%d", _ _FILE_ _, _ _LINE_ _);

To throw a class other than Exception, you just need to replace the zend_class_entry pointer in object_init_ex with one of your own creation.

To throw an exception that does not derive from Exception, you must create an object by hand and explicitly set EG (exception) to the object.

Using Custom Objects and Private Variables

I mentioned earlier in this chapter that storing private instance properties in the object's properties table is silly. Because the information is to be used only internally, and internally in an extension means that it is implemented in C, the ideal case would be for private variables to be native C types.

In PHP 5, generic objects are represented by the type zend_object and are stored in a global object store. When you call getThis(), the object handle ID stored in the calling object's zval representation is looked up in the object store. Conveniently, this object store can store more than zend_object types: It can actually store arbitrary data structures. This is useful for two reasons:

  • You can store resource data (such as database connection handles) directly in the object, without having to create and manage a resource for them.

  • You can store private class variables as a C struct alongside your object.

If you want custom object types, you need to create a custom class create_object function. When you instantiate a new object, the following steps occur:

1.
The raw object is created. By default this allocates and initializes an object, but with a custom creation function, arbitrary structures can be initialized.

2.
The newly created structure is inserted into the object store, and its ID is returned.

3.
The class constructor is called.

A creation function adheres to the following prototype:

zend_object_value (*create_object)(zend_class_entry *class_type TSRMLS_DC);

These are the key tasks the create_object function must attend to:

  • It must minimally create a zend_object structure.

  • It must allocate and initialize the property HashTable of the object.

  • It must store the object structure it creates in the object store, using zend_objects_store_put().

  • It must register a destructor.

  • It must return a zend_object_value structure.

Let's convert the Spread module from Chapter 21 without using resources and so it holds its connection handle in the object. Instead of using a standard zend_object structure, you should use an object that looks like this:

typedef struct {
  mailbox mbox;
  zend_object zo;
} spread_object;

If you allocate any memory inside the structure or create anything that needs to be cleaned up, you need a destructor to free it. Minimally, you need a destructor to free the actual object structures. Here is code for the simplest destructor possible:

static void spread_objects_dtor(void *object,
                                zend_object_handle handle TSRMLS_DC)
{
  zend_objects_destroy_object(object, handle TSRMLS_CC);
}

zend_objects_destroy_object() is used to destroy the allocated object itself.

You also need a clone function to specify how the object should respond if its _ _clone() method is called. Because a custom create_object handler implies that your stored object is not of standard type, you are forced to specify both of these functions. The engine has no way to determine a reasonable default behavior. Here is the clone function for the Spread extension:

static void spread_objects_clone(void *object, void **object_clone TSRMLS_DC){
  spread_object *intern = (spread_object *) object;
  spread_object **intern_clone = (spread_object **) object_clone;

  *intern_clone = emalloc(sizeof(spread_object));
  (*intern_clone)->zo.ce = intern->zo.ce;
  (*intern_clone)->zo.in_get = 0;
  (*intern_clone)->zo.in_set = 0;
  ALLOC_HASHTABLE((*intern_clone)->zo.properties);
  (*intern_clone)->mbox = intern->mbox;
}

object_clone is the new object to be created. Note that you basically deep-copy the clone data structure: You copy the ce class entry pointer and unset in_set and in_get, signifying that there is no active overloading in the object.

Then you need to have a create_object function. This function is very similar to the clone function. It allocates a new spread_object structure and sets it. Then it stores the resulting object in the object store, along with the destructor and clone handler.

Here is the custom object creator for the Spread extension:

zend_object_value spread_object_create(zend_class_entry *class_type TSRMLS_DC)
{
  zend_object_value retval;
  spread_object *intern;
  zend_object_handlers spread_object_handlers;

  memcpy(&spread_object_handlers,
         zend_get_std_object_handlers(),
         sizeof(zend_object_handlers));
  intern = emalloc(sizeof(spread_object));
  intern->zo.ce = class_type;
  intern->zo.in_get = 0;
  intern->zo.in_set = 0;

  ALLOC_HASHTABLE(intern->zo.properties);
  zend_hash_init(intern->zo.properties, 0, NULL, ZVAL_PTR_DTOR, 0);
  retval.handle = zend_objects_store_put(intern,
                                         spread_objects_dtor,
                                         spread_objects_clone);
  retval.handlers = &spread_object_handlers;
  return retval;
}

Now when you register the class, you need to specify this new create_object function:

static zend_class_entry *spread_ce_ptr;
static zend_function_entry spread_methods[] = {
  {NULL, NULL, NULL}
};

void register_spread()
{
  zend_class_entry ce;

  INIT_CLASS_ENTRY(ce, "Spread", spread_methods);
  ce.create_object = spread_object_create;
  spread_ce_ptr = zend_register_internal_class(&ce TSRMLS_CC);
}

To access this raw data, you use zend_object_store_get_object() to extract the entire object from the object store, as follows:

ZEND_METHOD(spread, disconnect)
{
  spread_object *sp_obj;
  mailbox mbox;

  sp_obj = (spread_object *) zend_object_store_get_object(getThis() TSRMLS_CC);
  mbox = sp_obj->mbox;
  sp_disconnect(mbox);
  sp_obj->mbox = -1;
}

zend_object_store_get_object() returns the actual object in the object store so that you can access the full struct. Converting the rest of the Spread extension to object-oriented code is left to you as an exercise; don't forget to add all the methods to Spread_methods.

Using Factory Methods

As discussed in Chapter 2, "Object-Oriented Programming Through Design Patterns," factory patterns can be very useful. In this context, a factory method simply needs to be a static class method that returns a new object. Here is a factory function that creates a Spread object:

PHP_FUNCTION(spread_factory)
{
  spread_object *intern;
  Z_TYPE_P(return_value) = IS_OBJECT;
  object_init_ex(return_value, spread_ce_ptr);
  return_value->refcount = 1;
  return_value->is_ref = 1;
  return;
}

You can then use this:

$obj = spread_factory();

in place of this:

$obj = new Spread;

Hiding Class Constructors

Sometimes you want to force users to use a constructor and prevent direct instantiation of a class via new. As in userspace PHP, the easiest way to accomplish this is to register a constructor and make it a private method. This prevents direct instantiation.


Creating and Implementing Interfaces

The final class feature covered in this chapter is defining and implementing interfaces. Internally, interfaces are basically classes that implement only abstract methods. To define an abstract method, you use the following macro:

ZEND_ABSTRACT_ME(class_name, method_name, argument_list);

class_name and method_name are obvious. argument_list is defined via the following macro blocks:

ZEND_BEGIN_ARG_INFO(argument_list, pass_by_ref)
ZEND_END_ARG_INFO()

This block defines argument_list and specifies whether its arguments are passed by reference. Internal to this block is an ordered list of parameters given by the following:

ZEND_ARG_INFO(pass_by_ref, name)

So to create the function entries for this PHP interface:

interface Foo {
  function bar($arg1, $arg2);
  function baz(&arg1);
}

you need to create both argument lists, as follows:

ZEND_BEGIN_ARG_INFO(bar_args, 0)
  ZEND_ARG_INFO(0, arg1)
  ZEND_ARG_INFO(0, arg2)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(baz_args, 0)
  ZEND_ARG_INFO(1, arg1)
ZEND_END_ARG_INFO()

You then need to create the methods table for Foo, as follows:

zend_function_entry foo_functions[] = {
  ZEND_ABSTRACT_METHOD(foo, bar, bar_args)
  ZEND_ABSTRACT_METHOD(foo, baz, baz_args)
  {NULL, NULL, NULL}
};

Finally, you use zend_register_internal_interface() to register Foo, as follows:

static zend_class_entry *foo_interface;

PHP_MINIT_FUNCTION(example)
{
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, "Foo", foo_functions)
    foo_interface = zend_register_internal_interface(&ce TSRMLS_CC);
    return SUCCESS;
}

That's all you need to do to register Foo as an interface.

Specifying that an extension class implements an interface is even simpler. The Zend API provides a single convenience function for declaring all the interfaces that the class implements:

void zend_class_implements(zend_class_entry *class_entry TSRMLS_DC,
                        int num_interfaces, ...);

Here class_entry is the class that implements interfaces. num_interfaces is the number of interfaces that you are implementing, and the variable argument is a list of pointers to zend_class_entry structures for the interfaces the class is implementing.


Previous
Table of Contents
Next