Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
Initial commit.
  • Loading branch information
jairbj committed Oct 23, 2016
0 parents commit b89e036
Show file tree
Hide file tree
Showing 54 changed files with 9,822 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/app/config/parameters.yml
/build/
/phpunit.xml
/var/*
!/var/cache
/var/cache/*
!var/cache/.gitkeep
!/var/logs
/var/logs/*
!var/logs/.gitkeep
!/var/sessions
/var/sessions/*
!var/sessions/.gitkeep
!var/SymfonyRequirements.php
/vendor/
/web/bundles/
/composer.phar
/.idea/
/README.pdf
/README.docx
/
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# HOOTSUITE Challenge #

## Challenge ##
[challenge.md]()


## What I did ##
I made a small project to simulate a webhook that receives messages and forward to previously defined destinations.
It was developed in PHP/Symfony3.
The project was made based in REST concepts and it answers the GET, POST, DELETE, PUT and PATCH requisitions.
The endpoints where tested using PHP Unit.
All requisitions to the webhook service are answered and should be made in JSON format.

## Considerations about security ##
Due to the short time, I didn’t implement neither validation to requests nor to sign the messages sent. Either I didn’t require HTTPS urls but it should be mandatory in a production environment.
Of course, in a production environment I’d use or RSA or ECC signature. I wouldn’t like to use HMAC as it’s use symmetric keys we need another step to ensure the keys are transferred in a secured way to the other side of endpoint.
Another option is authenticating requests to webhook based in credentials using JSON Web Tokens. The token can be transferred both in a “plain” way over HTTPS or either over a OAUTH layer of security.
The service also verifies if a valid URL was given and it won’t send messages to URLs that resolves to a private address.

## Considerations about scalability ##
Instead this project that relies in a centralized MySql the server can’t handle millions of connections, but it was developed with scalability in mind.
When a message is posted to the webhook it is added to a queue based on the destination.
The project has a worker module that consumes and process these messages and you can run multiple workers, each one processing one queue (one destination). The message ordering for each destination is guaranteed. You can also run a single worker to process all queues.
Each work can run in an independent server but it needs to connect to the same database server.
As it’s a small project (proof of concept only) I didn’t added validation to ensure you started only one worker for each queue. If you add more than one, the message ordering will not be guaranteed.
In another word, this project can scale as soon you have multiple destinations.
In a production environment, I’d use probably a queue server like RabbitMQ.

## First ##
Download composer and install the required dependencies with

php composer.phar install

## Instructions to run the server ##
1. Configure the database definitions in `./app/config/parameters.yml`
2. Create the database using the command `php ./bin/console doctrine:database:create`
3. Create database schema using the command `php ./bin/console doctrine:migrations:migrate`
4. Run the Symfony built In server using the command `php ./bin/console server:run`
The server will start listening in `http://127.0.0.1:8000`

## PHPUnit ##
There are PHPUnit tests in the `./tests` folder.
You can run those with the command: `./vendor/bin/phpunit`
Attention: Run the tests will erase the database.

## Webhook Requisitions (documentation) ##
The webhook documentation are located in both `doc.md` and `doc.html` files.
After start the server you can also read the documentation in `http://127.0.0.1:8000/doc`.
**All requests and responses should be made in JSON format.**

## Starting the message processor ##
The message processor can be started with the command

php ./bin/console message-processor
The service will start, process the messages and exit.
### Options: ###

--persistent
If set, the service will run in a persistent mode, it won’t exit until it’s cancelled.

--destination=DESTINATION
Indicates that the service should only process messages from the DESTINATION (destination id) queue. If this option isn’t set, the service will process messages from all queues.

--retry=RETRY
Indicates how many times (RETRY) the service will retry to deliver a message (in case of error) before removes it. Default = 3.

--retry-delay=RETRY-DELAY
Indicates how many time, in seconds (RETRY-DELAY), the service should wait before retry to deliver a message (in case of error). Default = 1.

**Attention:** The message processor will remove automatically messages that aren’t delivered for more than 24h. 

## Extra information ##
When you run the Symfony built in server, it automatically set environment as DEVELOPMENT, so in case of error it’ll return the full debug stack to the client. It doesn’t happen if environment is set to PRODUCTION.
As I didn’t use a queue server I decided to add GET and DELETE method to the “messages” endpoint, so we can check and eventually remove messages from the queue. The messages will remain in the queue only before they are processed.
If you remove a destination, it’ll automatically removes all the messages to this destination that hasn’t been already processed.
I really love backend programming and I really would like to be part of HootSuite team.
For me programming isn’t a job, is a pleasure.


7 changes: 7 additions & 0 deletions app/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
7 changes: 7 additions & 0 deletions app/AppCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;

class AppCache extends HttpCache
{
}
58 changes: 58 additions & 0 deletions app/AppKernel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),

new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
new \JMS\SerializerBundle\JMSSerializerBundle(),
new Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle(),
new Nelmio\ApiDocBundle\NelmioApiDocBundle(),


new AppBundle\AppBundle(),

];

if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}

return $bundles;
}

public function getRootDir()
{
return __DIR__;
}

public function getCacheDir()
{
return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
}

public function getLogDir()
{
return dirname(__DIR__).'/var/logs';
}

public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
}
}
38 changes: 38 additions & 0 deletions app/DoctrineMigrations/Version20161023151839.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Application\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;

/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20161023151839 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');

$this->addSql('CREATE TABLE destination (id INT AUTO_INCREMENT NOT NULL, url VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
$this->addSql('CREATE TABLE message (id INT AUTO_INCREMENT NOT NULL, destination_id INT NOT NULL, content_type VARCHAR(255) NOT NULL, msg_body LONGTEXT NOT NULL, created_at DATETIME NOT NULL, INDEX IDX_B6BD307F816C6140 (destination_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE message ADD CONSTRAINT FK_B6BD307F816C6140 FOREIGN KEY (destination_id) REFERENCES destination (id) ON DELETE CASCADE');
}

/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');

$this->addSql('ALTER TABLE message DROP FOREIGN KEY FK_B6BD307F816C6140');
$this->addSql('DROP TABLE destination');
$this->addSql('DROP TABLE message');
}
}
13 changes: 13 additions & 0 deletions app/Resources/views/base.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>
76 changes: 76 additions & 0 deletions app/Resources/views/default/index.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{% extends 'base.html.twig' %}

{% block body %}
<div id="wrapper">
<div id="container">
<div id="welcome">
<h1><span>Welcome to</span> Symfony {{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}</h1>
</div>

<div id="status">
<p>
<svg id="icon-status" width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1671 566q0 40-28 68l-724 724-136 136q-28 28-68 28t-68-28l-136-136-362-362q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 295 656-657q28-28 68-28t68 28l136 136q28 28 28 68z" fill="#759E1A"/></svg>

Your application is now ready. You can start working on it at:
<code>{{ base_dir }}</code>
</p>
</div>

<div id="next">
<h2>What's next?</h2>
<p>
<svg id="icon-book" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="-12.5 9 64 64" enable-background="new -12.5 9 64 64" xml:space="preserve">
<path fill="#AAA" d="M6.8,40.8c2.4,0.8,4.5-0.7,4.9-2.5c0.2-1.2-0.3-2.1-1.3-3.2l-0.8-0.8c-0.4-0.5-0.6-1.3-0.2-1.9
c0.4-0.5,0.9-0.8,1.8-0.5c1.3,0.4,1.9,1.3,2.9,2.2c-0.4,1.4-0.7,2.9-0.9,4.2l-0.2,1c-0.7,4-1.3,6.2-2.7,7.5
c-0.3,0.3-0.7,0.5-1.3,0.6c-0.3,0-0.4-0.3-0.4-0.3c0-0.3,0.2-0.3,0.3-0.4c0.2-0.1,0.5-0.3,0.4-0.8c0-0.7-0.6-1.3-1.3-1.3
c-0.6,0-1.4,0.6-1.4,1.7s1,1.9,2.4,1.8c0.8,0,2.5-0.3,4.2-2.5c2-2.5,2.5-5.4,2.9-7.4l0.5-2.8c0.3,0,0.5,0.1,0.8,0.1
c2.4,0.1,3.7-1.3,3.7-2.3c0-0.6-0.3-1.2-0.9-1.2c-0.4,0-0.8,0.3-1,0.8c-0.1,0.6,0.8,1.1,0.1,1.5c-0.5,0.3-1.4,0.6-2.7,0.4l0.3-1.3
c0.5-2.6,1-5.7,3.2-5.8c0.2,0,0.8,0,0.8,0.4c0,0.2,0,0.2-0.2,0.5c-0.2,0.3-0.3,0.4-0.2,0.7c0,0.7,0.5,1.1,1.2,1.1
c0.9,0,1.2-1,1.2-1.4c0-1.2-1.2-1.8-2.6-1.8c-1.5,0.1-2.8,0.9-3.7,2.1c-1.1,1.3-1.8,2.9-2.3,4.5c-0.9-0.8-1.6-1.8-3.1-2.3
c-1.1-0.7-2.3-0.5-3.4,0.3c-0.5,0.4-0.8,1-1,1.6c-0.4,1.5,0.4,2.9,0.8,3.4l0.9,1c0.2,0.2,0.6,0.8,0.4,1.5c-0.3,0.8-1.2,1.3-2.1,1
c-0.4-0.2-1-0.5-0.9-0.9c0.1-0.2,0.2-0.3,0.3-0.5s0.1-0.3,0.1-0.3c0.2-0.6-0.1-1.4-0.7-1.6c-0.6-0.2-1.2,0-1.3,0.8
C4.3,38.4,4.7,40,6.8,40.8z M46.1,20.9c0-4.2-3.2-7.5-7.1-7.5h-3.8C34.8,10.8,32.7,9,30.2,9L-2.3,9.1c-2.8,0.1-4.9,2.4-4.9,5.4
L-7,58.6c0,4.8,8.1,13.9,11.6,14.1l34.7-0.1c3.9,0,7-3.4,7-7.6L46.1,20.9z M-0.3,36.4c0-8.6,6.5-15.6,14.5-15.6
c8,0,14.5,7,14.5,15.6S22.1,52,14.2,52C6.1,52-0.3,45-0.3,36.4z M42.1,65.1c0,1.8-1.5,3.1-3.1,3.1H4.6c-0.7,0-3-1.8-4.5-4.4h30.4
c2.8,0,5-2.4,5-5.4V17.9h3.7c1.6,0,2.9,1.4,2.9,3.1V65.1L42.1,65.1z"/>
</svg>

Read the documentation to learn
<a href="http://symfony.com/doc/{{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION')[:3] }}/book/page_creation.html">
How to create your first page in Symfony
</a>
</p>
</div>

</div>
</div>
{% endblock %}

{% block stylesheets %}
<style>
body { background: #F5F5F5; font: 18px/1.5 sans-serif; }
h1, h2 { line-height: 1.2; margin: 0 0 .5em; }
h1 { font-size: 36px; }
h2 { font-size: 21px; margin-bottom: 1em; }
p { margin: 0 0 1em 0; }
a { color: #0000F0; }
a:hover { text-decoration: none; }
code { background: #F5F5F5; max-width: 100px; padding: 2px 6px; word-wrap: break-word; }
#wrapper { background: #FFF; margin: 1em auto; max-width: 800px; width: 95%; }
#container { padding: 2em; }
#welcome, #status { margin-bottom: 2em; }
#welcome h1 span { display: block; font-size: 75%; }
#icon-status, #icon-book { float: left; height: 64px; margin-right: 1em; margin-top: -4px; width: 64px; }
#icon-book { display: none; }
@media (min-width: 768px) {
#wrapper { width: 80%; margin: 2em auto; }
#icon-book { display: inline-block; }
#status a, #next a { display: block; }
@-webkit-keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
@keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
.sf-toolbar { opacity: 0; -webkit-animation: fade-in 1s .2s forwards; animation: fade-in 1s .2s forwards;}
}
</style>
{% endblock %}
11 changes: 11 additions & 0 deletions app/autoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

use Doctrine\Common\Annotations\AnnotationRegistry;
use Composer\Autoload\ClassLoader;

/** @var ClassLoader $loader */
$loader = require __DIR__.'/../vendor/autoload.php';

AnnotationRegistry::registerLoader([$loader, 'loadClass']);

return $loader;
74 changes: 74 additions & 0 deletions app/config/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }

# Put parameters here that don't need to change on each machine where the app is deployed
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
jms_serializer.camel_case_naming_strategy.class: JMS\Serializer\Naming\IdenticalPropertyNamingStrategy
locale: en

framework:
#esi: ~
#translator: { fallbacks: ["%locale%"] }
secret: "%secret%"
router:
resource: "%kernel.root_dir%/config/routing.yml"
strict_requirements: ~
form: ~
csrf_protection: ~
validation: { enable_annotations: true }
#serializer: { enable_annotations: true }
templating:
engines: ['twig']
default_locale: "%locale%"
trusted_hosts: ~
trusted_proxies: ~
session:
# http://symfony.com/doc/current/reference/configuration/framework.html#handler-id
handler_id: session.handler.native_file
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
fragments: ~
http_method_override: true
assets: ~

# Twig Configuration
twig:
debug: "%kernel.debug%"
strict_variables: "%kernel.debug%"

# Doctrine Configuration
doctrine:
dbal:
driver: pdo_mysql
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%"
charset: UTF8
# if using pdo_sqlite as your database driver:
# 1. add the path in parameters.yml
# e.g. database_path: "%kernel.root_dir%/data/data.db3"
# 2. Uncomment database_path in parameters.yml.dist
# 3. Uncomment next line:
# path: "%database_path%"

orm:
auto_generate_proxy_classes: "%kernel.debug%"
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true

# Swiftmailer Configuration
swiftmailer:
transport: "%mailer_transport%"
host: "%mailer_host%"
username: "%mailer_user%"
password: "%mailer_password%"
spool: { type: memory }

nelmio_api_doc:
name: "WebHook Documentation"
sandbox:
enabled: false
Loading

0 comments on commit b89e036

Please sign in to comment.