
- Image by hartboy via Flickr
My previous example in part 2 was just “hello world”, so I’m going to try for something more like the real world. You may find this example unusual, but it does work. I took the PageRange class I used in my July 2007 php|architect article and converted it to the JavaScript style. The class is a variation on Martin Fowler’s Range pattern, and is the hub of a re-implementation of the main functionality of PEAR Pager, using a more object-oriented style.
A Range object is defined by just two values (three in my variation), so it might seem like too much to have an object just to keep these values, but as you can see from the example, a Range class can have behaviors to change and compare ranges.
As before, I’m defining most of the methods inside the constructor.
<?php
class PageRange {
public $start;
public $end;
public $length;
private $moveToStartAt;
private $moveToEndAt;
private $changeLengthToEndAt;
private $includes;
private $isInside;
private $extendsBeyondStartOf;
private $extendsBeyondEndOf;
private $truncateToFitInside;
private $moveToFitInside;
private $asArray;
public function __construct($start,$end,$length) {
$self = $this;
$this->start = $start;
$this->end = $end;
$this->length = $length;
$this->moveToStartAt = function($start) use ($self) {
return PageRange::withStartAndLength(
$start,$self->length);
};
$this->moveToEndAt = function($end) use ($self) {
return PageRange::withEndAndLength($end,$self->length);
};
$this->changeLengthToEndAt = function($end) use ($self) {
return PageRange::withStartAndEnd($self->start,$end);
};
// Comparisons
$this->includes = function($page) use ($self) {
return $page >= $self->start
&& $page <= $self->end;
};
$this->isInside = function($range) use ($self) {
return $range->includes($self->getStart())
&& $range->includes($self->getEnd());
};
$this->extendsBeyondStartOf = function(PageRange $range) use ($self) {
return ($self->getStart() < $range->getStart());
};
$this->extendsBeyondEndOf = function(PageRange $range) use ($self) {
return ($self->getEnd() > $range->getEnd());
};
// Changes
$this->truncateToFitInside = function(PageRange $larger) use ($self) {
if ($self->isInside($larger)) return clone $self;
return $self->changeLengthToEndAt(
$larger->getEnd());
};
$this->moveToFitInside = function(PageRange $larger) use ($self) {
if ($self->isInside($larger)) return clone $self;
if ($self->extendsBeyondEndOf($larger))
return $self->moveToEndAt($larger->getEnd());
if ($self->extendsBeyondStartOf($larger))
return $self->moveToStartAt(
$larger->getStart());
};
// Convert to array
$this->asArray = function() use ($self) {
return range($self->start,$self->end);
};
}
I’ve defined the static methods in the usual way, non-JavaScript style. And there is, of course, the mandatory __call() method.
class PageRange...
public function __call($method,$args) {
return call_user_func_array($this->$method,$args);
}
public function getStart() { return $this->start; }
public function getEnd() { return $this->end; }
public function getLength() { return $this->length; }
private static function calculateEnd($start,$length) {
return $start + $length - 1;
}
private static function calculateStart($end,$length) {
return $end - $length + 1;
}
private static function calculateLength($start,$end) {
return $end - $start + 1;
}
public static function withStartAndLength(
$start,$length)
{
return new self(
$start,
self::calculateEnd($start,$length),
$length
);
}
public static function withEndAndLength($end,$length) {
return new self(
self::calculateStart($end,$length),
$end,
$length
);
}
public static function withStartAndEnd($start,$end) {
return new self(
$start,
$end,
self::calculateLength($start,$end)
);
}
}
An interesting aspect of this class is the fact that the constructor is private, so you have to use one of the creation methods. That’s part of my variation on the range pattern.
I’m wondering if this approach doesn’t have the same problem as in JavaScript, i.e. it is better to associate functions with an instance through the prototype (in PHP that would be the “old” way) than to create a function object for each instance of the class.
I mainly see functions bound to classes for monkey-patching than for use cases like yours when there is basically no benefit.