Skip to content

Instantly share code, notes, and snippets.

@nikic
Created October 13, 2012 11:22
Show Gist options
  • Save nikic/3884203 to your computer and use it in GitHub Desktop.
Save nikic/3884203 to your computer and use it in GitHub Desktop.
Analysis of getter/setter usage in Symfony and Zend Framework

In order to get a bit of "hard data" on what accessors will actually be used for once they are introduced I wrote a small script that scans through a codebase, finds all getter and setter methods and checks them for various characteristics. (The analyze.php file in this Gist.)

Here are the results of running it on a Symfony (Standard) skeleton.

absoluteTotal        => 18516 (486.6%)
total                =>  3805 (100.0%)
skipped              =>   124 (  3.3%)

get                  =>  2767 ( 72.7%, 100.0% of get)
getInterface         =>   274 (  7.2%,   9.9% of get)
trivialGet           =>   928 ( 24.4%,  33.5% of get)
simpleGet            =>  1172 ( 30.8%,  42.4% of get)
getWithCheck         =>   107 (  2.8%,   3.9% of get)

set                  =>   914 ( 24.0%, 100.0% of set)
setInterface         =>    92 (  2.4%,  10.1% of set)
trivialSet           =>   354 (  9.3%,  38.7% of set)
trivialSetChaining   =>    77 (  2.0%,   8.4% of set)
simpleSet            =>    49 (  1.3%,   5.4% of set)
simpleSetChaining    =>    16 (  0.4%,   1.8% of set)
setWithTypeHint      =>   289 (  7.6%,  31.6% of set)

unclassified         =>   612 ( 16.1%)

The same for Zend Framework:

absoluteTotal        =>  9876 (242.1%)
total                =>  4080 (100.0%)
skipped              =>    45 (  1.1%)

get                  =>  2480 ( 60.8%, 100.0% of get)
getInterface         =>   264 (  6.5%,  10.6% of get)
trivialGet           =>  1070 ( 26.2%,  43.1% of get)
simpleGet            =>   465 ( 11.4%,  18.8% of get)
getWithCheck         =>   266 (  6.5%,  10.7% of get)

set                  =>  1555 ( 38.1%, 100.0% of set)
setInterface         =>   138 (  3.4%,   8.9% of set)
trivialSet           =>   120 (  2.9%,   7.7% of set)
trivialSetChaining   =>   397 (  9.7%,  25.5% of set)
simpleSet            =>    14 (  0.3%,   0.9% of set)
simpleSetChaining    =>   189 (  4.6%,  12.2% of set)
setWithTypeHint      =>   342 (  8.4%,  22.0% of set)

unclassified         =>  1112 ( 27.3%)

An overview on what these stats mean and how I interpret them:

absoluteTotal, total:
    "absoluteTotal" is the total number of methods, "total" is the number of accessors,
    i.e. no-param methods starting with "get" or single-param methods starting with "set".
    As you can see getseter make up a large part of the methods. For Symfony 20% for
    ZF even 40%.
    => Getter and setter are a *very* common pattern, so it makes sense to do work in
    this direction.

get, set:
    "get" is the number of getters, "set" the number of setters. You can see that people
    use a good bit more getters than setters. For Symfony there are about 3x more getters,
    for ZF it's 1.6x as many.
    => It is common to have read-only data. This may either mean that there will be many
    accessors with asymmetric visibility or that many properties will only define a get
    accessor.
    
getInterface, setInterface:
    Number of getters/setters defined in interfaces or as abstract methods. This is about
    10% for both projects.
    => Accessor and property support in interfaces is necessary.
    
trivialGet, trivialSet, trivialSetChaining:
    "Trivial" means that the accessors act as a direct proxy to another property. The trivial
    set chaining variant also proxies to another property and additionally returns $this.
    For Symfony about 35% of all accessors are trivial. ZF also has 40% trivial getters, but
    seems to be rather fond of chaining, so 25% of the setters are trivially chaining.
    => A good chunk of all getters/setters don't really do anything. Once we get accessors they
    can be basically replaced by public properties. Accessors give the possibility to add more
    behavior lateron.
    
simpleGet, simpleSet, simpleSetChaining:
    A simple getter contains only a return statement, a simple setter only one assignment. For
    Symfony 40% of the getters are simple (this is not counting the trivial ones), for ZF it's
    20%.
    => Many getter are very simple and I think that especially the simple ones will be the ones
    converted to property accessors.
    
    This category is rather vague. Here are the three main types of simple getters I've found:
    1. Getters returning a constant value. This is more common than I would have thought. Good
       example of such a case are getName() methods that commonly return a constant string. Others
       return arrays or constants.
       => There seems to be need for read-only, constant properties.
    2. Proxies to other getters ($this->foo->getBar()).
    3. Getters taking some property and doing some small postprocessing, like replacing a string
       or similar.
       
getWithCheck:
    Getters which contain an if condition (containing a single statement), followed by a return.
    This is a common pattern for lazy loading. Amounts to 10% in ZF and 4% in Symfony.
    
setWithTypeHint:
    Setters where the argument is type hinted. 30% of the Symfony setters have one and 20% of
    the ZF ones.
    => There should be a simple way to typehint properties.
<?php
error_reporting(E_ALL);
ini_set('memory_limit', -1);
//$dir = '../Symfony';
$dir = '../ZendFramework-2.0.2';
require_once './lib/bootstrap.php';
$prettyPrinter = new PHPParser_PrettyPrinter_Zend;
class AccessorAccumulatingVisitor extends PHPParser_NodeVisitorAbstract {
public $accessorMethods = array();
public $absoluteTotal = 0;
public function enterNode(PHPParser_Node $node) {
if (!$node instanceof PHPParser_Node_Stmt_ClassMethod) return;
++$this->absoluteTotal;
if (0 === strpos($node->name, 'get')) {
if (0 !== count($node->params)) return;
} elseif (0 === strpos($node->name, 'set')) {
if (1 !== count($node->params)) return;
} else {
return;
}
// we don't care for doc comments
$node->setAttribute('comments', array());
$this->accessorMethods[] = $node;
echo $node->name, "\n";
}
}
$parser = new PHPParser_Parser(new PHPParser_Lexer());
$accumulator = new AccessorAccumulatingVisitor;
$traverser = new PHPParser_NodeTraverser;
$traverser->addVisitor($accumulator);
foreach (new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir),
RecursiveIteratorIterator::LEAVES_ONLY)
as $file
) {
if (!preg_match('/\.php$/', $file)) continue;
try {
$traverser->traverse($parser->parse(file_get_contents($file)));
} catch (PHPParser_Error $e) {}
}
$accessorMethods = $accumulator->accessorMethods;
file_put_contents('accessors', $prettyPrinter->prettyPrint($accessorMethods));
echo "\n";
//file_put_contents('accessors.serialized', serialize($accessorMethods));
//$accessorMethods = unserialize(file_get_contents('accessors.serialized'));
$total = count($accessorMethods);
$counts = array(
'absoluteTotal' => $accumulator->absoluteTotal,
'total' => $total,
'skipped' => 0,
'---',
'get' => 0,
'getInterface' => 0,
'trivialGet' => 0,
'simpleGet' => 0,
'getWithCheck' => 0,
'---',
'set' => 0,
'setInterface' => 0,
'trivialSet' => 0,
'trivialSetChaining' => 0,
'simpleSet' => 0,
'simpleSetChaining' => 0,
'setWithTypeHint' => 0,
'---',
'unclassified' => 0,
);
$skip = array_fill_keys(array('getIterator', 'set', 'get'), true);
$unclassifiedAccessorMethods = array();
foreach ($accessorMethods as $method) {
$name = $method->name;
$stmts = $method->stmts;
if (isset($skip[$name]) // Skip some special method names
|| (is_array($stmts) && empty($stmts)) // Skip empty methods
|| (isset($stmts[0]) && $stmts[0] instanceof PHPParser_Node_Stmt_Throw) // Skip methods that just throw
) {
++$counts['skipped'];
continue;
}
if ($name[0] === 'g') {
++$counts['get'];
if ($stmts === null) {
++$counts['getInterface'];
continue;
}
if ($stmts[0] instanceof PHPParser_Node_Stmt_Return) {
if ($stmts[0]->expr instanceof PHPParser_Node_Expr_PropertyFetch
|| $stmts[0]->expr instanceof PHPParser_Node_Expr_StaticPropertyFetch
) {
++$counts['trivialGet'];
} else {
++$counts['simpleGet'];
}
continue;
}
if (isset($stmts[1])
&& $stmts[0] instanceof PHPParser_Node_Stmt_If
&& count($stmts[0]->stmts) == 1
&& $stmts[1] instanceof PHPParser_Node_Stmt_Return
) {
++$counts['getWithCheck'];
continue;
}
} elseif ($name[0] === 's') {
++$counts['set'];
if ($stmts === null) {
++$counts['setInterface'];
continue;
}
if (null !== $method->params[0]->type) {
++$counts['setWithTypeHint'];
}
if ($stmts[0] instanceof PHPParser_Node_Expr_Assign
&& ($stmts[0]->var instanceof PHPParser_Node_Expr_PropertyFetch
|| $stmts[0]->var instanceof PHPParser_Node_Expr_StaticPropertyFetch)
) {
$isTrivial = $stmts[0]->expr instanceof PHPParser_Node_Expr_Variable;
if (1 === count($stmts)) {
if ($isTrivial) {
++$counts['trivialSet'];
} else {
++$counts['simpleSet'];
}
continue;
}
if (2 === count($stmts)
&& $stmts[1] instanceof PHPParser_Node_Stmt_Return
&& $stmts[1]->expr instanceof PHPParser_Node_Expr_Variable
&& $stmts[1]->expr->name === 'this'
) {
if ($isTrivial) {
++$counts['trivialSetChaining'];
} else {
++$counts['simpleSetChaining'];
}
continue;
}
}
}
++$counts['unclassified'];
$unclassifiedAccessorMethods[] = $method;
}
$getCount = $counts['get'];
$setCount = $counts['set'];
foreach ($counts as $name => $count) {
if ('---' === $count) {
echo "\n";
continue;
}
if (false !== stripos($name, 'get')) {
$additionalPercentage = sprintf(', %5.1f%% of get', $count / $getCount * 100);
} elseif (false !== stripos($name, 'set')) {
$additionalPercentage = sprintf(', %5.1f%% of set', $count / $setCount * 100);
} else {
$additionalPercentage = '';
}
echo sprintf("%-20s => %5d (%5.1f%%%s)\n", $name, $count, $count / $total * 100, $additionalPercentage);
}
file_put_contents('accessors.unclassified', $prettyPrinter->prettyPrint($unclassifiedAccessorMethods));
@schmittjoh
Copy link

If I have followed the discussion on the internals list correctly, using the property access would mean that performance drops down to the __get equivalent which is about 2 times slower than directly calling a method. I find this very discouraging from using this in any kind of project.

Additionally, the property access is not consistently possible due to the differences in method calls and property access in PHP:

class Author
{
     private $firstname {
         get() { return $this->firstname; }
         set($name) { $this->firstname = $name; }
     }

     private $lastname {
         get() { return $this->lastname; }
         set($name) { $this->lastname = $name; }
     }

     public function getFullName()
     {
         return $this->firstname.' '.$this->lastname;
     }
}

$author = new Author();
$author->firstname;
$author->lastname;
$author->fullname; // not possible
$author->getFullName();

Personally, this looks inconsistent to me. Sometimes you can use property access, other times you have to call a method explicitly. If you take a look at how this is implemented in Scala, it feels better:

class Author(var firstname, var lastname) {
    def fullname(): String = {
        return firstname + " " + lastname;
    }
}

val author = new Author("foo", "bar")
author.firstname;
author.lastname;
author.fullname; 

As you can see, I never need to use a method call. Scala will automatically convert it to a method call in case a method is present. It would be nice if a similar thing would be possible in PHP, but I guess due to PHP's dynamic nature that will be difficult without falling back to using __get and living with the added performance degradation.

@beberlei
Copy link

@schmittjoh: please check the RFC again, your usage of the getter setters functionality is wrong. And a property fullname that is read-only can be done.

Atm it should be something like:

class Author
{
private $lastname;
public $Lastname { .... };
}

@cpriest
Copy link

cpriest commented Oct 14, 2012

@schmittjoh: beberlei is right, there is no reason you cannot create a public fullname accessor to do the same thing as your function.

I also wanted to point out what is "realistic" about performance.

I did not test "method" vs "getter" access but I did test direct property access vs getter vs __get, here are the results (as posted to the internals list)

master
Cycles Direct Getter __get
v1.4 @ 10/8/2012 1m .05s .21s .20s

php 5.5.0-dev
Cycles Direct Getter __get
v1.4 @ 10/8/2012 1m .04s n/a .21s

So realistic performance here is that you can make 1 million getter calls in 200ms, I'm certain that could be improved but I'm pretty certain it's unlikely that anyone would need greater performance.

@cpriest
Copy link

cpriest commented Oct 14, 2012

@nikic: Really impressive analysis, I'm also happy with the results to see that property accessors are in wide-spread use now. My own projects use them generously as well.

@thbley
Copy link

thbley commented Jan 23, 2013

@nikic: Thanks for sharing! Could you add version numbers to the report?
In the code I see ZF 2.0.2, but what version of Symfony was tested? 2.0|1|2 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment