SOA with Symfony2 @ ConFoo 2014 in Montreal (CA)

10,771
-1

Published on

Symfony2 is one of the de-facto standards for developing enterprise-ready applications in PHP: being a very structured & decoupled framework, it becomes very handy and suitable for building Service Oriented architectures, which require loose coupling and a clean and tested structure: we will see hot to create a Service Oriented Architecture in Symfony2, taking advantage of messaging systems like RabbitMQ, HTTP APIs and Sf2's internals.

Published in: Technology
0 Comments
32 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
10,771
On Slideshare
0
From Embeds
0
Number of Embeds
13
Actions
Shares
0
Downloads
119
Comments
0
Likes
32
Embeds 0
No embeds

No notes for slide

SOA with Symfony2 @ ConFoo 2014 in Montreal (CA)

  1. 1. SOA with Symfony2 Alessandro Nadalin - Montreal, February 2014
  2. 2. This talk is for those...
  3. 3. Stuck with the legacy
  4. 4. dealing with CRONs
  5. 5. in the need of a solid foundation
  6. 6. rely on web services
  7. 7. need a pluggable software architecture
  8. 8. who love @fabpot
  9. 9. SOA
  10. 10. 1. SO WHAT? (A)
  11. 11. A software design based on discrete software components, "services", that collectively provide the functionalities of the larger software application
  12. 12. You typically start with the infamous PHP app, hopefully built with Symfony2 which does everything on its own
  13. 13. Then you realize that to provide a chat system to your users PHP might not be the best...
  14. 14. And soon you also decide, to improve performances, that your frontend should have its own in-memory persistence, to be faster and you put it into another service
  15. 15. Then, as always...
  16. 16. SCALE.
  17. 17. And eventually, your lead architect will come up and tell you that your Java-based chat sucks and should be replaced with...
  18. 18. NODEJS
  19. 19. In human-understandable words, SOA is a software design which embraces splitting a monolithic, totalitarian software architecture into smaller pieces, thus making them independent, loosely coupled and more maintainable
  20. 20. Ok, but in the real world?
  21. 21. 2. Your (Symfony2) app is just a piece of the puzzle
  22. 22. How does communication (usually) happen?
  23. 23. WEBSERVICES
  24. 24. Services can request data to other services, usually through WSs
  25. 25. POX
  26. 26. https://github.com/kriswallsmith/buzz https://github.com/fabpot/Goutte https://github.com/guzzle/guzzle
  27. 27. SOAP
  28. 28. PHP and SOAP in 2014 http://www.whitewashing.de/2014/01/31/soap_and_php_in_2014.html
  29. 29. HTTP
  30. 30. REST
  31. 31. FosREST https://github.com/FriendsOfSymfony/FOSRestBundle
  32. 32. # app/config/routing.yml users: type: rest resource: MyBundleUsersController
  33. 33. GET /users
  34. 34. class UsersController { public function getUsersAction() { return $this->get(‘storage’)->getUsers(); } }
  35. 35. class UsersController { public function getUsersAction() { return $this->get(‘storage’)->getUsers(); } }
  36. 36. class UsersController { public function getUsersAction() { return $this->get(‘storage’)->getUsers(); } }
  37. 37. View?
  38. 38. SERIALIZE ALL THE THINGS!
  39. 39. JMSSerializer Bundle: https://github.com/schmittjoh/JMSSerializerBundle
  40. 40. /** * @ExclusionPolicy("all") */ class Customer implements UserInterface, EquatableInterface { /** * @Expose * @SerializedName("addresses") * @Groups({"Customer:depth:1"}) */ protected $addresses; /** * @Expose * @Groups({ * "Customer:list", * "Customer:detail", * }) */ private $email;
  41. 41. /** * @ExclusionPolicy("all") */ class Customer implements UserInterface, EquatableInterface { /** * @Expose * @SerializedName("addresses") * @Groups({"Customer:depth:1"}) */ protected $addresses; /** * @Expose * @Groups({ * "Customer:list", * "Customer:detail", * }) */ private $email;
  42. 42. /** * @ExclusionPolicy("all") */ class Customer implements UserInterface, EquatableInterface { /** * @Expose * @SerializedName("addresses") * @Groups({"Customer:depth:1"}) */ protected $addresses; /** * @Expose * @Groups({ * "Customer:list", * "Customer:detail", * }) */ private $email;
  43. 43. /** * @ExclusionPolicy("all") */ class Customer implements UserInterface, EquatableInterface { /** * @Expose * @SerializedName("addresses") * @Groups({"Customer:depth:1"}) */ protected $addresses; /** * @Expose * @Groups({ * "Customer:list", * "Customer:detail", * }) */ private $email;
  44. 44. /** * @ExclusionPolicy("all") */ class Customer implements UserInterface, EquatableInterface { /** * @Expose * @SerializedName("addresses") * @Groups({"Customer:depth:1"}) */ protected $addresses; /** * @Expose * @Groups({ * "Customer:list", * "Customer:detail", * }) */ private $email;
  45. 45. /** * @ExclusionPolicy("all") */ class Customer implements UserInterface, EquatableInterface { /** * @Expose * @SerializedName("addresses") * @Groups({"Customer:depth:1"}) */ protected $addresses; /** * @Expose * @Groups({ * "Customer:list", * "Customer:detail", * }) */ private $email;
  46. 46. EVENTS
  47. 47. services notify the architecture that an event has happened
  48. 48. asynchronous messaging queues
  49. 49. RabbitMQ https://github.com/videlalvaro/rabbitmqbundle
  50. 50. old_sound_rabbit_mq: connections: default: host: 'localhost' port: 5672 user: 'guest' password: 'guest' vhost: '/' lazy: false producers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} consumers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} queue_options: {name: 'userreg'} callback: mailer
  51. 51. old_sound_rabbit_mq: connections: default: host: 'localhost' port: 5672 user: 'guest' password: 'guest' vhost: '/' lazy: false producers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} consumers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} queue_options: {name: 'userreg'} callback: mailer
  52. 52. old_sound_rabbit_mq: connections: default: host: 'localhost' port: 5672 user: 'guest' password: 'guest' vhost: '/' lazy: false producers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} consumers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} queue_options: {name: 'userreg'} callback: mailer
  53. 53. old_sound_rabbit_mq: connections: default: host: 'localhost' port: 5672 user: 'guest' password: 'guest' vhost: '/' lazy: false producers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} consumers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} queue_options: {name: 'userreg'} callback: mailer
  54. 54. old_sound_rabbit_mq: connections: default: host: 'localhost' port: 5672 user: 'guest' password: 'guest' vhost: '/' lazy: false producers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} consumers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} queue_options: {name: 'userreg'} callback: mailer
  55. 55. mailer: class: "MyMailer" arguments: [..., ...]
  56. 56. $this->get('old_sound_rabbit_mq.user_registration_producer') ->publish(serialize($msg));
  57. 57. php app/console rabbitmq:consumer user_registration
  58. 58. old_sound_rabbit_mq: connections: default: host: 'localhost' port: 5672 user: 'guest' password: 'guest' vhost: '/' lazy: false producers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} consumers: user_registration: connection: default exchange_options: {name: 'userreg', type: direct} queue_options: {name: 'userreg'} callback: mailer
  59. 59. class Mailer { … public function execute(AMQPMessage $message) { // do stuff } }
  60. 60. class Mailer { … public function execute(AMQPMessage $message) { // do stuff } }
  61. 61. 2. Free data
  62. 62. CONSIDER ELIMINATING FK CONSTRAINTS
  63. 63. A service might need to handle data with another DBMS, so FKs are virtually impossible
  64. 64. ABSTRACT THE DATA
  65. 65. You might think in "rows" but the architecture thinks in "resources"
  66. 66. $this->get(‘doctrine’) ->getRepository(‘My:Entity’) ->findActiveOnes()
  67. 67. $this->get(‘doctrine’) ->getRepository(‘My:Entity’) ->findActiveOnes()
  68. 68. $this->get(‘storage’) ->getRepository(‘My:Entity’) ->findActiveOnes()
  69. 69. class RedisStorage { public function getRepository($name) { $this->hash = $name; return $this; } public function findActiveOnes() { $results = $this->redis->hget($this->hash); return array_filter($results, function($r){ return $r[‘active’] == true; }); } }
  70. 70. class RedisStorage { public function getRepository($name) { $this->hash = $name; return $this; } public function findActiveOnes() { $results = $this->redis->hget($this->hash); return array_filter($results, function($r){ return $r[‘active’] == true; }); } }
  71. 71. class RedisStorage { public function getRepository($name) { $this->hash = $name; return $this; } public function findActiveOnes() { $results = $this->redis->hget($this->hash); return array_filter($results, function($r){ return $r[‘active’] == true; }); } }
  72. 72. class RedisStorage { public function getRepository($name) { $this->hash = $name; return $this; } public function findActiveOnes() { $results = $this->redis->hget($this->hash); return array_filter($results, function($r){ return $r[‘active’] == true; }); } }
  73. 73. class RedisStorage { public function getRepository($name) { $this->hash = $name; return $this; } public function findActiveOnes() { $results = $this->redis->hget($this->hash); return array_filter($results, function($r){ return $r[‘active’] == true; }); } }
  74. 74. class RedisStorage { public function getRepository($name) { $this->hash = $name; return $this; } public function findActiveOnes() { $results = $this->redis->hget($this->hash); return array_filter($results, function($r){ return $r[‘active’] == true; }); } }
  75. 75. REPOSITORIES NEED INTERFACES
  76. 76. ENTITIES NEED INTERFACES
  77. 77. forget managers, you need collections
  78. 78. implements StockStorageInterface
  79. 79. use StockStorageInterface as Storage; class RedisStorage implements Storage { ...
  80. 80. use StockStorageInterface as Storage; class RedisStorage implements Storage { ...
  81. 81. use StockStorageInterface as Storage; class StockStorage implements Storage { ...
  82. 82. collections as a service
  83. 83. stock: class: “MyNamespaceStockStorage” arguments: … … $this->get(‘stock’)->findActiveOnes();
  84. 84. stock: class: “MyNamespaceStockStorage” arguments: … … $this->get(‘stock’)->findActiveOnes();
  85. 85. stock: class: “MyNamespaceStockStorage” arguments: … … $this->get(‘stock’)->findActiveOnes();
  86. 86. No more FKs and the ability of JOINing to retrieve some related data
  87. 87. But you choose what perfectly fits each service: your transactions over a RDBMS and your community over a graph DB
  88. 88. So complicated!
  89. 89. Have fun returning serialized collections over HTTPS in ~50ms with Doctrine!
  90. 90. 3. Standardize
  91. 91. EVERY DEVELOPER NEEDS THE ENTIRE ARCHITECTURE ON HIS MACHINE
  92. 92. The architecture needs to be installed in ~1 hour
  93. 93. Setting up VMs is an hassle and they are so slow!
  94. 94. go #vagrant
  95. 95. But Vagrant is still suboptimal: provisioning and system resources are still a pain!
  96. 96. 4. Identity
  97. 97. Centralized authentication = identity service
  98. 98. OAuth
  99. 99. OpenID
  100. 100. JWS
  101. 101. JSON WEB SIGNATURE
  102. 102. JSON WEB TOKEN
  103. 103. JSON WEB SIGNATURE
  104. 104. JAVASCRIPT OBJECT SIGNING & ENCRYPTION
  105. 105. JOSE http://www.thread-safe.com/2012/03/json-object-signing-and-encryption-jose.html
  106. 106. 1. The user enters the credentials once in your frontend 2. The JS app will forward them to your Auth webservice JS APP AUTH SERVICE 3. The Auth webservice will then generate the encrypted JWS and set a cookie with its value JS APP 4. The JS app can now just execute calls using that cookie
  107. 107. 1. The user enters the credentials once in your frontend JS APP
  108. 108. 2. The JS app will forward them to your Auth webservice JS APP AUTH SERVICE
  109. 109. AUTH SERVICE 3. The Auth webservice will then generate the encrypted JWS and set a cookie with its value
  110. 110. AUTH SERVICE JS APP 4. The JS app can now just execute calls using that cookie
  111. 111. 1. The user enters the credentials once in your frontend 2. The JS app will forward them to your Auth webservice JS APP AUTH SERVICE 3. The Auth webservice will then generate the encrypted JWS and set a cookie with its value JS APP 4. The JS app can now just execute calls using that cookie
  112. 112. setcookie($name, $jws,$ttl, $path, $domain, true);
  113. 113. setcookie($name, $jws,$ttl, $path, $domain, true); HTTPS
  114. 114. JWS in PHP?
  115. 115. namshi/jose
  116. 116. use NamshiJOSEJWS; $jws = new JWS('RS256'); $jws->setPayload(array( 'uid' => $user->getid(), )); $privateKey = openssl_get_privatekey("file://path/to/private. key"); $jws->sign($privateKey); setcookie('identity', $jws->getTokenString()); use NamshiJOSEJWS; $jws = JWS::load($_COOKIE['identity']); $public_key = openssl_pkey_get_public("/path/to/public.key"); if ($jws->verify($public_key)) { echo "EUREKA!; }
  117. 117. use NamshiJOSEJWS; $jws = new JWS('RS256'); $jws->setPayload(array( 'uid' => $user->getid(), )); $privateKey = openssl_get_privatekey("file://path/to/private. key"); $jws->sign($privateKey); setcookie('identity', $jws->getTokenString()); use NamshiJOSEJWS; $jws = JWS::load($_COOKIE['identity']); $public_key = openssl_pkey_get_public("/path/to/public.key"); if ($jws->verify($public_key)) { echo "EUREKA!; }
  118. 118. use NamshiJOSEJWS; $jws = new JWS('RS256'); $jws->setPayload(array( 'uid' => $user->getid(), )); $privateKey = openssl_get_privatekey("file://path/to/private. key"); $jws->sign($privateKey); setcookie('identity', $jws->getTokenString()); use NamshiJOSEJWS; $jws = JWS::load($_COOKIE['identity']); $public_key = openssl_pkey_get_public("/path/to/public.key"); if ($jws->verify($public_key)) { echo "EUREKA!; }
  119. 119. use NamshiJOSEJWS; $jws = new JWS('RS256'); $jws->setPayload(array( 'uid' => $user->getid(), )); $privateKey = openssl_get_privatekey("file://path/to/private. key"); $jws->sign($privateKey); setcookie('identity', $jws->getTokenString(), ...); use NamshiJOSEJWS; $jws = JWS::load($_COOKIE['identity']); $public_key = openssl_pkey_get_public("/path/to/public.key"); if ($jws->verify($public_key)) { echo "EUREKA!; }
  120. 120. use NamshiJOSEJWS; $jws = new JWS('RS256'); $jws->setPayload(array( 'uid' => $user->getid(), )); $privateKey = openssl_get_privatekey("file://path/to/private. key"); $jws->sign($privateKey); setcookie('identity', $jws->getTokenString()); use NamshiJOSEJWS; $jws = JWS::load($_COOKIE['identity']); $public_key = openssl_pkey_get_public("/path/to/public.key"); if ($jws->verify($public_key)) { echo "EUREKA!; }
  121. 121. use NamshiJOSEJWS; $jws = new JWS('RS256'); $jws->setPayload(array( 'uid' => $user->getid(), )); $privateKey = openssl_get_privatekey("file://path/to/private. key"); $jws->sign($privateKey); setcookie('identity', $jws->getTokenString()); use NamshiJOSEJWS; $jws = JWS::load($_COOKIE['identity']); $public_key = openssl_pkey_get_public("/path/to/public.key"); if ($jws->verify($public_key)) { echo "EUREKA!; }
  122. 122. ...what about Symfony2?
  123. 123. use SymfonyComponentSecurity...AuthenticationProviderInterface; class JwsProvider implements AuthenticationProviderInterface { ... public function authenticate(TokenInterface $token) { $key = openssl_pkey_get_public($this->publicKeyPath); $jws = $token->getJws(); if ($key && $jws->isValid($key)) { $token->setUser(User::fromArray($jws->getPayload())); return $token; } throw new AuthenticationException('authentication failed.'); } ... }
  124. 124. use SymfonyComponentSecurity...AuthenticationProviderInterface; class JwsProvider implements AuthenticationProviderInterface { ... public function authenticate(TokenInterface $token) { $key = openssl_pkey_get_public($this->publicKeyPath); $jws = $token->getJws(); if ($key && $jws->isValid($key)) { $token->setUser(User::fromArray($jws->getPayload())); return $token; } throw new AuthenticationException('authentication failed.'); } ... }
  125. 125. use SymfonyComponentSecurity...AuthenticationProviderInterface; class JwsProvider implements AuthenticationProviderInterface { ... public function authenticate(TokenInterface $token) { $key = openssl_pkey_get_public($this->publicKeyPath); $jws = $token->getJws(); if ($key && $jws->isValid($key)) { $token->setUser(User::fromArray($jws->getPayload())); return $token; } throw new AuthenticationException('authentication failed.'); } ... }
  126. 126. use SymfonyComponentSecurity...AuthenticationProviderInterface; class JwsProvider implements AuthenticationProviderInterface { ... public function authenticate(TokenInterface $token) { $key = openssl_pkey_get_public($this->publicKeyPath); $jws = $token->getJws(); if ($key && $jws->isValid($key)) { $token->setUser(User::fromArray($jws->getPayload())); return $token; } throw new AuthenticationException('authentication failed.'); } ... }
  127. 127. use SymfonyComponentSecurity...AuthenticationProviderInterface; class JwsProvider implements AuthenticationProviderInterface { ... public function authenticate(TokenInterface $token) { $key = openssl_pkey_get_public($this->publicKeyPath); $jws = $token->getJws(); if ($key && $jws->isValid($key)) { $token->setUser(User::fromArray($jws->getPayload())); return $token; } throw new AuthenticationException('authentication failed.'); } ... }
  128. 128. I can't simply use the HTTP basic authentication, it was so convenient!
  129. 129. ...and flawed. Modern apps, modern tech.
  130. 130. 4. Embrace messaging
  131. 131. Don't wait, notify instead
  132. 132. Different services can intercept an even, separately
  133. 133. If one is down, the others keep working
  134. 134. Who cares about milliseconds for notifications?
  135. 135. The human body is the bottleneck
  136. 136. Email?
  137. 137. SMS?
  138. 138. Be reliable
  139. 139. “Daemons are great”
  140. 140. “Daemons are great” - No PHP developer ever
  141. 141. SUPERVISOR http://supervisord.org/
  142. 142. SUPERVISE http://cr.yp.to/daemontools/supervise.html
  143. 143. use python ;-)
  144. 144. It doesn’t matter...
  145. 145. ‫اﻟﺤﺮوف اﻟﻌﺮﺑﯿﺔ ‪if you talk‬‬
  146. 146. Rabbit makes everyone talk the same language
  147. 147. chat sync daemons Batch processing frontend ERP agony transcoding telcom
  148. 148. But I Symfony2
  149. 149. Tech monogamy is so ‘90 “given a hammer, everything becomes a nail”
  150. 150. One size doesn’t fit all
  151. 151. “But look at Google, they basically use python for everything”
  152. 152. “...and C”
  153. 153. “...and C++”
  154. 154. “...and Java”
  155. 155. “...and JavaScript”
  156. 156. “...and Go”
  157. 157. But hey, you say...
  158. 158. they really dislike supporting multiple platforms
  159. 159. “...and Dart”
  160. 160. But hey, you say...
  161. 161. they really are not into supporting the secondary platforms
  162. 162. “...and AngularJS”
  163. 163. 5. Not always sunday
  164. 164. Monitor in real time
  165. 165. Native support for Symfony2
  166. 166. Logs are first-class citizens
  167. 167. https://github.com/Seldaek/monolog
  168. 168. Sharp, as much as possible
  169. 169. I LIED A LOOOOOT
  170. 170. Symfony isn’t even the main point of this SOA talk
  171. 171. You can build SOAs with anything
  172. 172. ...or can you?
  173. 173. http://odino.org/why-we-choose-symfony2-over-any-other-php-framework/
  174. 174. By being decoupled and HTTP-centric Symfony2 has turned into an ideal application framework that can take (part of) the stage in a SOA
  175. 175. Full-stack is dead
  176. 176. PHP developers are dead
  177. 177. LONG LIVE API ENGINEERS!
  178. 178. All in all...
  179. 179. SOA is complex
  180. 180. like Symfony2
  181. 181. A puzzle with more pieces
  182. 182. like Symfony2
  183. 183. More things to keep in mind
  184. 184. like Symfony2
  185. 185. COMPLEX IS NOT COMPLICATED
  186. 186. Loose coupling
  187. 187. every service is independent, not forced to the constraints of a monolithic block
  188. 188. you have the freedom of changing or replacing services without the hassle of touching an entire system
  189. 189. State-of-the-art defense against outages
  190. 190. Fault tolerance
  191. 191. if one of the services has an outage, the rest of the architecture still works
  192. 192. if a service, listening for messages, is down, the publisher doesn't get stuck
  193. 193. Cleaner architecture
  194. 194. SoC happens at architectural, not application, level and you can perform large-scale refactorings without the fear of destroying the entire system
  195. 195. ...yawn...
  196. 196. Alessandro Nadalin
  197. 197. Alessandro Nadalin @_odino_
  198. 198. Alessandro Nadalin @_odino_ Namshi | Rocket Internet
  199. 199. Alessandro Nadalin @_odino_ Namshi | Rocket Internet VP Technology
  200. 200. Alessandro Nadalin @_odino_ Namshi | Rocket Internet VP Technology odino.org
  201. 201. Thanks! Alessandro Nadalin @_odino_ Namshi | Rocket Internet VP Technology odino.org
  202. 202. By the way
  203. 203. Wanna join?
  204. 204. We are looking for talented nerds!
  205. 205. We are looking for talented nerds! frontend engineer
  206. 206. We are looking for talented nerds! frontend engineer data engineer
  207. 207. We are looking for talented nerds! lead frontend engineer data engineer
  208. 208. Thanks! Alessandro Nadalin @_odino_ Namshi | Rocket Internet VP Technology odino.org
  209. 209. Image credits http://www.flickr.com/photos/randystiefer/6998037429/sizes/h/in/photostream/ http://www.flickr.com/photos/55432818@N02/5500963965/ http://www.flickr.com/photos/pamhule/4503305775/ http://www.flickr.com/photos/wili/1427890704/ http://www.flickr.com/photos/nickpiggott/5212959770/sizes/l/in/photostream/ http://www.flickr.com/photos/nomad9491/2549965427/sizes/l/in/photostream/ http://www.flickr.com/photos/amyvdh/95764607/sizes/l/in/photostream/ http://www.flickr.com/photos/matthoult/4524176654/ http://www.flickr.com/photos/kittyeden/2416355396/sizes/l/in/photostream/ http://www.flickr.com/photos/jpverkamp/3078094381/ http://www.flickr.com/photos/madpoet_one/5554416836/ http://www.flickr.com/photos/87792096@N00/2732978107/ http://www.flickr.com/photos/petriv/4787037035/ http://www.flickr.com/photos/51035796522@N01/111091247/sizes/l/in/photostream/ http://www.flickr.com/photos/m-i-k-e/6366787693/sizes/l/in/photostream/ http://www.flickr.com/photos/39065466@N04/9111005211/ http://www.flickr.com/photos/marchorowitz/5449945176/sizes/l/in/photolist-9iAoQ1-8s4ueH-bCWef9-bCWdPh-e48XUmbu67nh-a7xaEr-8wLiNh-9aYU1k-9F4VUN-dYqzr1-9vosHb-8BtFuw-8P3h2e-9tqc6M-82qpt4-7UgkBJ-dgSnfS-aJiubZ-9Xji2U-9UVpkC7BSh7Y-8GE54k-91GHtB-8VMHJ2-8wiwvo-aCmPCg-925Tg8-bcBv9T-dGUseY/ http://www.flickr.com/photos/blegg/745322703/sizes/l/in/photostream/ http://www.flickr.com/photos/centralasian/4649550142/sizes/l/in/photostream/ http://www.flickr.com/photos/pennstatelive/4947279459/sizes/l/in/photostream/ http://www.flickr.com/photos/tjblackwell/7819341478/ http://www.flickr.com/photos/brainbitch/6066375386/ http://www.flickr.com/photos/nnova/4215594009/ http://www.flickr.com/photos/publicenergy/2246574379/ http://www.flickr.com/photos/andrewteman/4592833017/sizes/o/in/photostream/ http://www.flickr.com/photos/beautifulrevelry/8548004964/sizes/o/in/photostream/ http://www.flickr.com/photos/denaldo/5066810104/sizes/l/in/photostream/ http://www.flickr.com/photos/picturewendy/8365723674/sizes/l/in/photostream/ http://www.flickr.com/photos/danielygo/6644679037/sizes/l/in/photostream/ http://www.flickr.com/photos/ross/7614352/sizes/l/in/photostream/ http://www.flickr.com/photos/75932013@N02/6874087329/sizes/l/in/photostream/ http://crucifixjel.deviantart.com/art/300-Wallpaper-03-66516887 https://www.flickr.com/photos/acidsaturation/6635987033/sizes/l/

×