Propilex
A Silex application which uses Propel, Backbone.JS, but also:
- Bower as browser package manager;
- RequireJS;
- Garlic.js;
- Moment.js;
- Twitter Bootstrap;
- Less CSS;
- Backbone.Forms;
- Keymaster.
And:
This project also features:
Installation
Install PHP dependencies:
composer install
And browser dependencies using Bower:
bower install
Build Model classes, SQL, and Propel's configuration:
cp app/config/propel/runtime-conf.xml.dist app/config/propel/runtime-conf.xml
cp app/config/propel/build.properties.dist app/config/propel/build.properties
bin/bootstrap
You're done! You can run the application using the PHP built-in webserver:
php -S 0.0.0.0:4000 -t web/
Open http://localhost:4000/
in your browser to see Propilex running.
Docker
Build the container:
docker build -t propilex .
Run it:
docker run -itP propilex
Retrieve the public port mapped to the container's port 80:
docker port $(docker ps -aql 1) 80
Open http://localhost:<port>/
, and profit!
Usage
This application is a truely RESTful API with hypermedia links, content negotiation (format and language) but also cache on safe methods. The API is HAL compliant, and serves content in either XML or JSON format, using the most appropriate language (English, French, etc.) based on clients' preferences.
You can use the web interface, or the command line and tools such as HTTPie or cURL.
GET
You can get either a set of documents, or a single document. The response for
the set of documents is cacheable, relying on the ETag
headers.
Getting all documents in JSON:
$ http http://localhost:4000/documents Accept:application/hal+json
HTTP/1.1 200 OK
Cache-Control: public
Content-Type: application/hal+json
ETag: "c4ca4238a0b923820dcc509a6f75849b"
{
"_links": {
"curies": [
{
"href": "http://localhost:4000/rels/{rel}",
"name": "p",
"templated": true
}
],
"p:documents": {
"href": "http://localhost:4000/documents"
},
"first": {
"href": "http://localhost:4000/documents?page=1&limit=10"
},
"last": {
"href": "http://localhost:4000/documents?page=1&limit=10"
},
"self": {
"href": "http://localhost:4000/documents?page=1&limit=10"
}
},
"_embedded": {
"documents": [
{
"_links": {
"self": {
"href": "http://localhost:4000/documents/1"
},
"curies": [
{
"href": "http://localhost:4000/rels/{rel}",
"name": "p",
"templated": true
}
]
},
"body": "Hello, World!",
"created_at": "2013-12-22 17:55:18",
"id": 1,
"title": "Hello!",
"updated_at": "2013-12-22 17:55:18"
},
{
"_links": {
"self": {
"href": "http://localhost:4000/documents/2"
},
"curies": [
{
"href": "http://localhost:4000/rels/{rel}",
"name": "p",
"templated": true
}
]
},
"body": "This is a body",
"created_at": "2013-12-22 17:55:22",
"id": 2,
"title": "This is a title",
"updated_at": "2013-12-22 22:09:37"
}
]
},
"limit": 10,
"page": 1,
"pages": 1
}
Getting all documents in XML:
$ http http://localhost:4000/documents Accept:application/hal+xml
HTTP/1.1 200 OK
Cache-Control: public
Content-Type: application/hal+xml
ETag: "c4ca4238a0b923820dcc509a6f75849b"
<?xml version="1.0" encoding="UTF-8"?>
<collection href="http://localhost:4000/documents?page=1&limit=10" limit="10" page="1" pages="1">
<resource href="http://localhost:4000/documents/1" rel="documents">
<id>1</id>
<title>Hello!</title>
<body>Hello, World!</body>
<created_at><![CDATA[2013-12-22 17:55:18]]></created_at>
<updated_at><![CDATA[2013-12-22 17:55:18]]></updated_at>
<link href="http://localhost:4000/rels/{rel}" name="p" rel="curies" templated="1"/>
</resource>
<resource href="http://localhost:4000/documents/2" rel="documents">
<id>2</id>
<title>This is a title</title>
<body>This is a body</body>
<created_at><![CDATA[2013-12-22 17:55:22]]></created_at>
<updated_at><![CDATA[2013-12-22 22:09:37]]></updated_at>
<link href="http://localhost:4000/rels/{rel}" name="p" rel="curies" templated="1"/>
</resource>
<link href="http://localhost:4000/documents?page=1&limit=10" rel="first"></link>
<link href="http://localhost:4000/documents?page=1&limit=10" rel="last"></link>
<link href="http://localhost:4000/rels/{rel}" name="p" rel="curies" templated="1"/>
<link href="http://localhost:4000/documents" rel="p:documents"/>
</collection>
Getting a single document in JSON:
$ http http://localhost:4000/documents/1 Accept:application/hal+json
HTTP/1.1 200 OK
Content-Type: application/hal+json
{
"_links": {
"self": {
"href": "http://localhost:4000/documents/1"
},
"curies": [
{
"href": "http://localhost:4000/rels/{rel}",
"name": "p",
"templated": true
}
]
},
"body": "Hello, World!",
"created_at": "2013-12-22 17:55:18",
"id": 1,
"title": "Hello!",
"updated_at": "2013-12-22 22:41:55"
}
Getting a single document in XML:
$ http http://localhost:4000/documents/1 Accept:application/hal+xml
HTTP/1.1 200 OK
Content-Type: application/hal+xml
<?xml version="1.0" ?>
<document href="http://localhost:4000/documents/1">
<id>1</id>
<title><![CDATA[Hello!]]></title>
<body><![CDATA[Hello, World!]]></body>
<created_at><![CDATA[2013-12-22 17:55:18]]></created_at>
<updated_at><![CDATA[2013-12-22 22:41:55]]></updated_at>
<link href="http://localhost:4000/rels/{rel}" name="p" rel="curies" templated="1"/>
</document>
POST
You can create a new document by sending JSON data:
$ curl -H 'Accept: application/hal+json' -H 'Content-Type: application/json' \
-d '{"title": "Hello!", "body": "JSON"}' \
http://localhost:4000/documents
HTTP/1.1 201 Created
Content-Type: application/hal+json
Location: http://localhost:4000/documents/7"
{
"id": 7,
"title": "Hello!",
"body": "JSON",
"created_at": "2013-12-22 22:48:46",
"updated_at": "2013-12-22 22:48:46",
"_links": {
"self": {
"href": "http://localhost:4000/documents/7"
},
"curies": [
{
"href": "http://localhost:4000/rels/{rel}",
"name": "p",
"templated": true
}
]
}
}
Creating a new document is also doable by sending XML data:
$ curl -H 'Accept: application/hal+json' -H 'Content-Type: application/xml' \
-d '<document><title>Hello!</title><body>XML</body></document>' \
http://localhost:4000/documents
HTTP/1.1 201 Created
Content-Type: application/hal+json
Location: http://localhost:4000/documents/8"
{
"id": 8,
"title": "Hello!",
"body": "XML",
"created_at": "2013-12-22 22:50:46",
"updated_at": "2013-12-22 22:50:46",
"_links": {
"self": {
"href": "http://localhost:4000/documents/8"
},
"curies": [
{
"href": "http://localhost:4000/rels/{rel}",
"name": "p",
"templated": true
}
]
}
}
### DELETE
$ http DELETE http://localhost:4000/documents/1
HTTP/1.1 204 No Content
If the document you are trying to delete does not exist, you will get an error:
$ http DELETE http://localhost:4000/documents/70 Accept:application/hal+json
HTTP/1.1 404 Not Found
Content-Type: application/vnd.error+json
{
"message": "Document with id = 70 does not exist."
}
XML response for this error:
$ http DELETE http://localhost:4000/documents/70 Accept:application/hal+xml
HTTP/1.1 404 Not Found
Content-Type: application/vnd.error+xml
<?xml version="1.0" ?>
<resource>
<message><![CDATA[Document with id = 70 does not exist.]]></message>
</resource>
Translations & Error Messages
Both error messages or application's messages are translated depending on the
Accept-Language
header. In order to implement this, you need to use the
StackNegotiation middleware,
and a Silex application's before
middleware.
A response with a status code equals to either 404
or 500
follows the
vnd.error specification.
You will get an error message if you try to get an unknown document:
$ http GET http://localhost:4000/documents/123 Accept:application/hal+json Accept-Language:en
HTTP/1.1 404 Not Found
Content-Type: application/vnd.error+json
{
"message": "Document with id = \"123\" does not exist."
}
XML response for this error:
$ http GET http://localhost:4000/documents/123 Accept:application/hal+xml Accept-Language:fr
HTTP/1.1 404 Not Found
Content-Type: application/vnd.error+xml
<?xml version="1.0" ?>
<resource>
<message><![CDATA[Le document avec id = "123" n'existe pas.]]></message>
</resource>
You will get an error message if you submit invalid data in order to create or update documents:
$ http POST http://localhost:4000/documents Accept:application/hal+json Accept-Language:fr
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"errors": [
{
"field": "title",
"message": "Cette valeur ne doit pas être vide."
},
{
"field": "body",
"message": "Cette valeur ne doit pas être vide."
}
]
}
XML response for this error:
$ curl -H 'Accept: application/hal+xml' -H 'Content-Type: application/json' \
-d '{"title": "Hello!"}' \
http://localhost:4000/documents
HTTP/1.1 400 Bad Request
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<errors>
<error field="body">
<message><![CDATA[This value should not be blank.]]></message>
</error>
</errors>
If you send an Accept
header with an unsupported mime type, you will get a
406
error:
$ http GET http://propilex.herokuapp.com/documents Accept:application/json
HTTP/1.1 406 Not Acceptable
Content-Type: text/plain
Mime type "application/json" is not supported. Supported mime types are:
application/hal+xml, application/hal+json.
Configuration
All configuration files are located in the app/config/
directory.
propel/runtime-conf.xml
andpropel/build.properties
contain the database configuration, if you modify it, don't forget to rebuild things by using the previous command;serializer/*
contains the Serializer and Hateoas configuration;messages.*.yml
contain the translations;validation.yml
contains the Validation configuration.
You can also find a few parameters in app/propilex.php
.
Screenshots
Deploy on Heroku
Create a new Heroku application:
heroku create --buildpack https://github.com/CHH/heroku-buildpack-php myapp
Deploy it!
git push heroku master
Unit Tests
First, install the application as described in section Installation.
Backend
Install dev dependencies:
composer install --dev
Then, run the test suite:
bin/phpunit
Frontend
In a browser, open /js/tests/index.html
.
In a shell, install PhantomJS, and run the following comand:
phantomjs web/js/tests/run-qunit.js file://`pwd`/web/js/tests/index.html
License
Propilex is released under the MIT License. See the bundled LICENSE file for details.