It’s hard to imagine pushing the limits of object oriented PHP so far that your web servers choke, but the truth is those limits are reached faster than you think. We’ve run some tests over at Wufoo and it turns out that any sort of mass object creation is pretty much not going to work at scale. The problem is this limit on object creation forces developers to balance code consistency, which is desirable—especially for the old-schoolers, with performance. While replacing objects with arrays when possible makes things a little better, the most performance friendly approach involves appending strings. For your convenience, we’ve run some tests that measure page execution times and memory usage to create the following guideline to help you plan out what areas of your code may have to break away from an object oriented nature.
Basically, we set up a simple PHP page to iterate over a loop and create 1) a giant concatenated string, 2) an array of arrays containing the word ‘test’, and 3) an array of objects with one variable set to ‘test’.
As the table shows, creating objects takes a good amount of memory and time when compared to a string or array. And since 7.7mb is nearing the default memory limit for PHP, the page is about to time out. Sure, the memory limit can be increased, but the point is that there is still a maximum. For most pages, 11,000 objects is overkill. But in some cases, like exporting a hefty database to CSV format or returning data from a public API, we may want to return 50,000+ records.
The good news is that the memory limitation does not put us in a place where the advantages of reusable code is completely removed. We just have to take a few precautions when writing code that has intensive looping or object creation.
unset() — Avoid getting at objects through the accessor, and work directly with the recordset loop. After each iteration, unset the object that was just created. After a quick test, 45,000 objects could be created and unset using 1.43mb of memory at peak.
Static Method Calls — A static method can be called without the need to instantiate an object. For example, if our desired output is an XML string, but we need some of the objects helper functions, we can still call functions off of the objects statically with Object::function(). Doing so only increased memory usage from 1.45mb to 1.46mb in the string example.
Paging — The downside to unset() and static methods is that we have to bypass our accessors to get at the objects and work directly with the loop. A third option is to be strict with paging, and never allow for more than X number of objects to be returned. This will keep the code 100% consistent, but may be a bit more intensive on the database and increase page execution time.
Why Does this Matter?
Many development techniques, such as domain driven design, keep a set of value objects along with an accessor to each of those objects. The accessors act as an API that make retrieval of the objects easy and consistent. For example, let’s say we have a User object, and a UserAccessor — we would call UserAccessor->loadAllUsers(), which would return to us an array of User objects. Then, we can loop through the array, and display a list of all users and have access to the member variables and functions.
This is great because all developers work with the same objects, and SQL queries are constrained to accessors. Even if this isn’t your preferred development style, chances are your object oriented approach strives to achieve similar levels of consistency. That said, we can see from the benchmarks above that there may be situations where a portion of code we’ve created cannot return an array of objects because of memory limitations. Overall, object memory usage is far from a deal breaker, but it is something every developer worried about scaling should be aware of.
For more information, here are a couple of links that may provide some more insight.