Pascal - February 28, 2013

PHP has reached its limit

Edit: Chose a much better title

Web developers need to get stuff done. Web development is constantly changing and the pressure from clients can be pretty high. Because, hey, it can’t be so hard to output that HTML and a bit of information from the database.

Some time ago I switched to Ruby from PHP, because I was getting more and more frustrated. This meant to find new clients and learn some basic things from scratch. But it was rewarding. PHP strangely worked for many years but, although still many developers are using it, its time is over. It’s over because PHP has reached its limit where it simply cannot compete with other languages, specifically Python and Ruby. The requirements and expectations are getting higher and PHP cannot meet those requirements. Yeah, I’m telling you nothing new here, I know. But if you’re in your comfort zone, in your everyday job, writing PHP based websites it’s just hard to realize how bad it is. It only struck me when a new framework came up, TYPO3 Flow. I’ll leave the definition whether it’s a Ruby on Rails clone or not up to you, it doesn’t really matter. What does matter: it is written by some of the brightest and most motivated heads in the PHP community. It took them five years to experiment and find something that works for them. Please keep that in mind when you read the examples below: these are not just some schoolkids writing their first CMS - these are smart men and women.

While I could complain in a long, theoretical article, I prefer code to prove my point. Below you’ll see some standard examples of tasks a web developer has to do every day. Basic CRUD (create, read, update, destroy) of objects, routing, rendering templates. I compare Flow with Rails because I know both of them and they make nearly the same claims: convention over configuration, MVC, ORM. I took the examples from the Flow documentation and wrote the Rails counterpart.

I think these examples make clear why PHP has reached its limit. Metaprogramming and reflection have to be implemented by the framework as they’re not part of the language. All annotation that does not manipulate the functionality was removed by me. Yes, there is annotation that changes the logic of the programm.

CRUD

Define a property

Flow
/**
 * @var string
 * @Flow\Validate(type="Text")
 * @Flow\Validate(type="StringLength", options={ "minimum"=1, "maximum"=80 })
 * @ORM\Column(length=80)
 */
protected $title = '';

public function getTitle() {
  return $this->title;
}

public function setTitle($title) {
  $this->title = $title;
}
Rails
field :title
validates_length_of :title, minimum: 1, maximum: 80

Define a relation

Flow
/**
 * @var \Doctrine\Common\Collections\Collection<\TYPO3\Blog\Domain\Model\Post>
 * @ORM\OneToMany(mappedBy="blog")
 * @ORM\OrderBy({"date" = "DESC"})
 */
protected $posts;

public function __construct() {
        $this->posts = new \Doctrine\Common\Collections\ArrayCollection();
}

/**
 * @param \TYPO3\Blog\Domain\Model\Post $post
 */
public function addPost(\TYPO3\Blog\Domain\Model\Post $post) {
        $post->setBlog($this);
        $this->posts->add($post);
}

/**
 * @param \TYPO3\Blog\Domain\Model\Post $post
 */
public function removePost(\TYPO3\Blog\Domain\Model\Post $post) {
        $this->posts->removeElement($post);
}

public function getPosts() {
        return $this->posts;
}
Rails
// in the blog model
has_many :posts

// in the post model
belongs_to :blog

Fetch a record

Domain driven Design causes some overhead here.

Flow:
// Repository.php
namespace TYPO3\Blog\Domain\Repository;

/**
 * A repository for Blogs
 *
 * @Flow\Scope("singleton")
 */
class BlogRepository extends \TYPO3\Flow\Persistence\Repository {
}

// In the Controller.php
/**
 * @Flow\Inject
 * @var \TYPO3\Blog\Domain\Repository\BlogRepository
 */
protected $blogRepository;

…
$this->blogRepository->findByUid($myUid);
Rails
Blog.find(my_uid)

Find something specific

Flow
// Repository.php
class PostRepository extends \TYPO3\Flow\Persistence\Repository {

    /**
     * @param \TYPO3\Blog\Domain\Model\Tag $tag
     * @param \TYPO3\Blog\Domain\Model\Blog $blog The blog the post must refer to
     * @return \TYPO3\Flow\Persistence\QueryResultInterface The posts
     */
    public function findByTagAndBlog(\TYPO3\Blog\Domain\Model\Tag $tag,
      \TYPO3\Blog\Domain\Model\Blog $blog) {
        $query = $this->createQuery();
        return $query->matching(
            $query->logicalAnd(
                $query->equals('blog', $blog),
                $query->contains('tags', $tag)
            )
        )
        ->setOrderings(array(
            'date' => \TYPO3\Flow\Persistence\QueryInterface::ORDER_DESCENDING)
        )
        ->execute();
    }
}

// In the Controller.php
 /**
  * @Flow\Inject
  * @var \TYPO3\Blog\Domain\Repository\PostRepository
  */
 protected $postRepository;
 
 …
 $this->postRepository->findByTagAndBlog($tag, $blog);
Rails
Post.where(tags: tag, blog: blog).desc(:date)

Create a record (updating and destroying are similar)

Flow
// Repository.php
namespace TYPO3\Blog\Domain\Repository;

/**
 * A repository for Blogs
 *
 * @Flow\Scope("singleton")
 */
class BlogRepository extends \TYPO3\Flow\Persistence\Repository {
}

// In the Controller.php
 /**
  * @Flow\Inject
  * @var \TYPO3\Blog\Domain\Repository\BlogRepository
  
  */
 protected $blogRepository;
 
 …
 $blog = new \TYPO3\Blog\Domain\Model\Blog();
 $blog->title = 'My title';
 $this->blogRepository->add($blog);
Rails
Blog.create(title: 'My Title')

View

Assign something to the view

Flow
$posts = …;
$this->view->assign('posts', $posts);
Rails
@posts = ...

Templates

Flow
 <f:if condition="{post.numberOfComments} > 0">
         <f:then>
                 <f:if condition="{post.numberOfComments} == 1">
                         <f:then>{post.numberOfComments} comment</f:then>
                         <f:else>{post.numberOfComments} comments</f:else>
                 </f:if>
         </f:then>
         <f:else>No comments</f:else>
 </f:if>
Rails (with erb)
<% if @post.comments.any? %>
	<%= pluralize(@post.comments.count, 'comment') %>
<% else %>
	No comments
<% end %>

Essentially ERB is Embedded Ruby. You can write everything that is Ruby. Just like <? and ?> in PHP. Of course it is properly escaped.

Link to a post

Flow
<f:link.action action="show" controller="Post" arguments="{post: post}">{post.title}</f:link.action>
Rails
<%= link_to @post.title, @post %>

With i18n

Flow
<my:viewhelper><f:translate key="mykey"/></my:viewhelper>
Rails
<%= my_viewhelper t('mykey') %>

Routes

Static route

Flow
-
  name: 'Static demo route'
  uriPattern: 'my/demo'
  defaults:
    '@package':    'My.Demo'
    '@controller': 'Product'
    '@action':     'list'
Rails
get '/my/demo' => 'Product#list'

Dynamic route

Flow
-
  name: 'Dynamic demo route with parameter'
  uriPattern: 'products/list/{sortOrder}.{@format}'
  defaults:
    '@package':    'My.Demo'
    '@controller': 'Product'
    '@action':     'list'

Rails

get '/products/list/:sort_order.:format' => 'product#list'

Constraints

Flow
class RegexRoutePartHandler extends \TYPO3\Flow\Mvc\Routing\DynamicRoutePart {

        /**
         * @param string $requestPath value to match, the string to be checked
         * @return boolean TRUE if value could be matched successfully, otherwise FALSE.
         */
        protected function matchValue($requestPath) {
                if (!preg_match($this->options['pattern'], $requestPath, $matches)) {
                        return FALSE;
                }
                $this->value = array_shift($matches);
                return TRUE;
        }

        /**
         * @param string $value The route part (must be a string)
         * @return boolean TRUE if value could be resolved successfully, otherwise FALSE.
         */
        protected function resolveValue($value) {
                if (!is_string($value) || !preg_match($this->options['pattern'], $value, $matches)) {
                        return FALSE;
                }
                $this->value = array_shift($matches);
                return TRUE;
        }

}

And in the routes configuration:

-
  name: 'RegEx route - only matches index & list actions'
  uriPattern: 'blogs/{blog}/{@action}'
  defaults:
    '@package':    'My.Blog'
    '@controller': 'Blog'
  routeParts:
    '@action':
      handler:   'My\Blog\RoutePartHandlers\RegexRoutePartHandler'
      options:
        pattern: '/index|list/'
Rails
get '/blog/:blog_id/:action' => 'blog#:action', constraints: { :blog_id => /index|list/ }

Aspect oriented programming

AOP is one of the key features of Flow. You can manipulate virtually any class with it. From the documentation:

Flow
namespace Examples\Forum\Logging;

/**
 * @Flow\Aspect
 */
class LoggingAspect {

        /**
         * @Flow\Inject
         * @var \Examples\Forum\Logger\ApplicationLoggerInterface
         */
        protected $applicationLogger;

        /**
         * @param \TYPO3\Flow\AOP\JoinPointInterface $joinPoint
         * @Flow\Before("method(Examples\Forum\Domain\Model\Forum->deletePost())")
         * @return void
         */
        public function logDeletePost(\TYPO3\Flow\AOP\JoinPointInterface $joinPoint) {
                $post = $joinPoint->getMethodArgument('post');
                $this->applicationLogger->log('Removing post ' . $post->getTitle(), LOG_INFO);
        }

}
Rails
class Post
    extend ActiveModel::Callbacks
    define_model_callbacks :destroy, only: [:before]
    
    before_destroy :log
    
    def log
    	Logger.info("Removing post #{self}")
    end
end

Of course after and around filters also exist.

Now, I could go on and on with this list. As I said these is only the everyday standard challanges you encounter when writing web apps. It really gets worse when your problems get more complex. Again: this is nothing against TYPO3 Flow or the authors. It just shows that PHP has reached its limit.

F43f1f2103a47483067969e5cc955b4b?size=120

I’m pretty fuckin’ far from OK.

Loading comments...

Please sign in to post a comment.