PHPCR Browser: Edit Support and Better Performance
The PHPCR Browser, initially released in late 2013, has been completely rewritten to improve performance and reliability. It is now ready for prime time, and today we're announcing the immediate release of PHPCR Browser 1.0.
A few months ago, my first assignment as a fresh marmelab employee was to build a browser for PHPCR repositories. Using Silex, AngularJS, and or course PHPCR, I wrote a proof of concept web application in about a week, and we released it with a open-source license to get feedback from the community.
I had no AngularJS skills at the time, but it was a good opportunity to learn it the hard way. To be honest the first version wasn't very good. After that, I worked on a customer project for a couple months, again with AngularJS. I did peer programming sessions with senior developers, and discussed about the architecture with my fellow coworkers. I also understood the AngularJS best practices. I learned a lot.
The PHPCRBrowser offered basic reading features, but the performance was poor. In order to allow node edition, API reusability, better performance, and robustness, I chose the obvious: rewrite the AngularJS part from scratch.
Let me remind you of the initial architecture:
- Backend: Silex Application, composed of:
- API: performs communication between the Model and the PHPCR implementation (Jackrabbit only for now).
- Provider: exposes the API as a HTTP REST service
- A few Silex routes consuming the HTTP service internally
- Lots of AngularJS code running on the browser, consuming data from the HTTP service
The first version wasn't a true single page application. The frontend relied on a few PHP controllers, and AngularJS didn't manage the routing.
I decided to remove PHP from the frontend entirely. The Silex app now only exposes our REST API, and a static bootstrap route for the AngularJS application at
The JSON responses from the Silex web service used to be manipulated as raw objects by the AngularJS app (using Restangular). I added an
ObjectMapper layer on top of it, to make model logic testable and reusable. As much as possible, we are trying to share the same object model between the frontend and the backend. Our mapper exposes a
find(path) method, and returns the appropriate Model instance:
// We can now deal with our repository using a Repository object
// We can now deal with our workspace using a Workspace object
// We can now ... you got it
Each element in the graph can now communicate with other elements using methods like
I also moved the Routing responsibility from Silex to AngularJS. The webapp now retrieves the current repository/workspace/node from the URL, and converts it to model objects using a
Lastly, all our services use the promise pattern. This keeps asynchronous usage easy, code is clearer, and it fits well with external dependencies like Restangular.
We have a powerful service layer, and the same object model shared between the backend and the frontend. Building a a great interface on top of that shouldn't be too difficult.
For this new version, I wanted to follow best practices as much as possible. That's why the browser now uses Compass and the Sass version of Bootstrap 3.
Furthermore, the key here is usability. Manipulating nodes in a graph can be complex (think: editing an XML document). In the case of the PHPCR Browser, in a CMS context, it should be straightforward.
The application is composed of three views:
- Repository list
- Workspace list (for a given repository)
- Node list (for a given workspace)
Each of these views should offer CRUD actions to some degree. For instance, the workspace list should allow workspace deletion if the owner repository supports it. But how to add a delete action without any confirmation modal, and avoiding unwanted deletion? My answer, in this case, was drag & drop. I added a trash area on the bottom part of the screen; users just have to drag a workspace there to delete it.
The Node list view is subdivided into two parts: a tree view on the left, and a property list on the right. The only way to get a robust and testable tree feature was to split the tree model (hooked with the HTTP service using events) from the tree view (redrawn by AngularJS using two way data binding). That also allowed me to add more sophisticated interactions (drag and drop for node move and removal, node addition).
The browser is pretty fast. But I had to face two performance pain points:
- The tree view can be memory expensive to display
- Each part of the application produces a request to the HTTP Service when it needs data
The first one was solved by writing a dedicated service to cache the tree object, shared between controllers on the AngularJS side.
For the second issue, the solution was actually quite easy. The web application relies on a comunication layer called
ApiFoundation, depending on
Restangular for the actual HTTP communication. Restangular is built on top of the
$http service provided by AngularJS, and supports native HTTP cache. So I just had to enable cache support on Restangular, and add a
Cache-Control header in all JSON responses sent by the provider. The performance boost was immediate.
Lastly, preparing the data in the backend was done in a naive way, with a major impact on response time. Some queries were doubled, and counting of children implied hydration of the entire child list. Using step-by-step profiling on a very large repository, I quickly detected the slowdowns, and fixed them using better algorithms.
As a result, the navigation throughout the application is snappy, including the tree view, even on large repositories. See for instance this screencast on a repository with more than 100K nodes:
The new version of PHPCR Browser is feature complete, unit tested, fast, and secure. Using test-driven development allowed for a better, decoupled architecture, prevent bugs, and facilitate maintenance. I am very proud of the result. It's definitely worthy of a 1.0 tag, and ready for real world usage on all your document trees.
There is still a lot of work to do to handle more use cases in the future. With the new clean AngularJS layer, it should be not too difficult for contributors to understand the project. Please feel free to contribute, and send me your feedback on the GitHub repository!