Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# For more information about the properties used in
# this file, please see the EditorConfig documentation:
# http://editorconfig.org/

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.yml]
indent_size = 2
indent_style = space

[*.{yml,json}]
# The indent size used in the `package.json` file cannot be changed
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
indent_size = 2
indent_style = space

[composer.json]
indent_size = 4
10 changes: 10 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
SS_ENVIRONMENT_TYPE="dev"
SS_DEFAULT_ADMIN_USERNAME="admin"
SS_DEFAULT_ADMIN_PASSWORD="admin"

# DB credentials
SS_DATABASE_CLASS="MySQLPDODatabase"
SS_DATABASE_SERVER="mysql"
SS_DATABASE_USERNAME="root"
SS_DATABASE_PASSWORD="root"
SS_DATABASE_NAME="catch"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

/vendor/
/themes/cache/
/public/resources/
/themes/catch/node_modules/
/silverstripe-cache/*
2 changes: 2 additions & 0 deletions .htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
RewriteEngine On
RewriteRule ^(.*)$ public/$1
56 changes: 39 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,47 @@

A simple test for PHP / JavaScript Developers

## Instructions
## Installation

1. Fork or clone this repo
2. Write a script to Import the CSV file located in `data/customers.csv` into a database (MySQL or Mongo is preferred)
3. Create a basic PHP web service that serves the data from the database as JSON
4. Create a basic web page that asynchronously loads the JSON into a list or table when you click a button
5. If you are completing this test as part of a job application please include a zip file of your project (including git config/metadata) with your application otherwise create a pull request and we'll take a look :)
Install Docker if it is not already \
Open up a tab in the terminal, cd into the project root and run \
`silverstripe-docker/develop.sh build` \
then \
`silverstripe-docker/develop.sh up`

### Guidelines
Which will get all the docker containers running

1. Your repo needs to include at minimum anything required to get the app working. Detailed instructions should be provided in the `README.md` file to setup and run the app.
2. If a structured schema migration tool is not used then a setup script must be supplied to create any data tables etc
3. Try not spend more than 2 hours on it
### IN A NEW TAB

### Bonus Points
In a new terminal window run the following commands from the project root.

* Make it Pretty
* Make it as OO as possible
* Consume dependencies with tools like Composer, Bower and NPM
* Use patterns like MVC, ORM
* Compile any front end assets with a build tool like gulp
* Unit tests
run \
`silverstripe-docker/develop.sh composer install` \
To Install all php dependencies

run \
`silverstripe-docker/develop.sh npm install` \
to install all front end dependencies

run \
`silverstripe-docker/develop.sh npm run production` \
to bundle the front end assets

run \
`silverstripe-docker/develop.sh devbuild` \
to build the database

Then run \
`silverstripe-docker/develop.sh sake CSVImportTask` \
to run the import CSV script which pulls the customer data into the database

Then the page with the button should be available on

http://localhost:8888/

if something on the page is not loading, try running

http://localhost:8888/?flush=all

When Finished go back to the terminal tab running the Docker containers
and run ctrl-c then `silverstripe-docker/develop.sh down`
3 changes: 3 additions & 0 deletions app/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<FilesMatch "\.(php|php3|php4|php5|phtml|inc)$">
Deny from all
</FilesMatch>
18 changes: 18 additions & 0 deletions app/_config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

use SilverStripe\Security\PasswordValidator;
use SilverStripe\Security\Member;

if (isset($_GET['d']) && ($db = $_GET['d'])) {
global $databaseConfig;
if ($db == 'l') {
$databaseConfig['type'] = 'SQLite3Database';
$databaseConfig['path'] = ':memory:';
}
}

// remove PasswordValidator for SilverStripe 5.0
$validator = PasswordValidator::create();
$validator->setMinLength(8);
$validator->setHistoricCount(6);
Member::set_password_validator($validator);
5 changes: 5 additions & 0 deletions app/_config/mysite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
Name: myproject
---
SilverStripe\Core\Manifest\ModuleManifest:
project: app
8 changes: 8 additions & 0 deletions app/_config/theme.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
Name: mytheme
---
SilverStripe\View\SSViewer:
themes:
- '$public'
- 'catch'
- '$default'
88 changes: 88 additions & 0 deletions app/src/CSVImportTask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php
namespace sasky\catchTest\tasks;

use SilverStripe\Dev\BuildTask;
use Illuminate\Support\Collection;
use SilverStripe\ORM\Map;
use sasky\catchTest\app\Customer;

class CSVImportTask extends BuildTask
{
private static $segment = 'CSVImportTask';

protected $title = 'CSV Import Task';
protected $description = 'Imports all the CSV Data';

public function run($request)
{
$customersToImport = $this->convertCSVIntoArray();

// Mapping the Customer Keys so they conform to SS DB conventions
$customerDBMap = [
'first_name' => 'FirstName',
'last_name' => 'LastName',
'email' => 'Email',
'gender'=> 'Gender',
'ip_address' => 'IPAddress',
'company' => 'Company',
'city' => 'City',
'title' => 'Title',
'website' => 'Website'
];

foreach ($customersToImport as $customerToImport) {
// if a customer is found with a matching email then assume this record
// has already been created
if (Customer::get()->filter('Email', $customerToImport['email'])->first()) {
continue;
}
$customer = Customer::create();
foreach ($customerToImport as $customerKey => $customerValue) {
$customer->setField($customerDBMap[$customerKey], $customerValue);
}
$customer->write();
}
}

/**
* Converts the supplied CSV file into an array
* @return array a list of customers
*/

private function convertCSVIntoArray()
{
// I know there are functions for doing similar things in php like str_getcsv
// But I just wanted to have a crack at doing it manually for fun.

// note: using the Collection class from Laravel
// takes a bit of getting used to, but I think it makes cleaner code
// than the nested foreach loops you would usually see in php code like this
// inspirited by https://adamwathan.me/refactoring-to-collections/

$csvString = file_get_contents('/var/www/html/data/customers.csv');
$collection = new Collection(explode("\n", $csvString));

$headerTitles = $collection->filter(function ($item, $key) {
return $key === 0;
})->map(function ($item) {
return explode(',', $item);
})->first();

return $collection->values()->reject(function ($item, $key) {
// reject the first row as it's the header row
// also reject any item returns falsy
return $key === 0 || !$item;
})->map(function ($item) {
$array = explode(',', $item);
return $array;
})->map(function ($item, $key) use ($headerTitles) {
// build an associative array with the header titles as keys
return array_combine($headerTitles, $item);
})->map(function ($item, $key) {
// remove the id column's as SS will set that when a record is created
// as long as we keep the list in order the csv id's should match up to the DB record's that are about to be created
unset($item['id']);
return $item;
})->toArray();
}
}
46 changes: 46 additions & 0 deletions app/src/Customer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
namespace sasky\catchTest\app;

use SilverStripe\ORM\DataObject;

class Customer extends DataObject
{
private static $db = [
'FirstName' => 'Varchar(255)',
'LastName' => 'Varchar(255)',
'Email' => 'Varchar(255)',
'Gender' => 'Varchar(255)',
'IPAddress' => 'Varchar(255)',
'Company' => 'Varchar(255)',
'City' => 'Varchar(255)',
'Title' => 'Varchar(255)',
'Website' => 'Text',
];

private static $table_name = 'Customer';

public static function get_all_records()
{
$records = [];
foreach (self::get() as $customer) {
$records[] = $customer->getRecord();
}
return $records;
}

public function getRecord()
{
$record = [];
$record['id'] = $this->ID ? $this->ID : '';
$record['first_name'] = $this->FirstName ? $this->FirstName : '';
$record['last_name'] = $this->LastName ? $this->LastName : '';
$record['email'] = $this->Email ? $this->Email : '';
$record['gender'] = $this->Gender ? $this->Gender : '';
$record['ip_address'] = $this->IPAddress ? $this->IPAddress : '';
$record['company'] = $this->Company ? $this->Company : '';
$record['city'] = $this->City ? $this->City : '';
$record['title'] = $this->Title ? $this->Title : '';
$record['website'] = $this->Website ? $this->Website : '';
return $record;
}
}
13 changes: 13 additions & 0 deletions app/src/Page.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace {

use SilverStripe\CMS\Model\SiteTree;

class Page extends SiteTree
{
private static $db = [];

private static $has_one = [];
}
}
17 changes: 17 additions & 0 deletions app/src/PageController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace {

use SilverStripe\CMS\Controllers\ContentController;
use sasky\catchTest\app\Customer;

class PageController extends ContentController
{
private static $allowed_actions = ['getcustomers'];

public function getcustomers()
{
return json_encode(Customer::get_all_records());
}
}
}
39 changes: 39 additions & 0 deletions app/tests/CSVImportTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace sasky\catchTest\tests;

use SilverStripe\Dev\FunctionalTest;
use sasky\catchTest\app\Customer;

class CSVImportTest extends FunctionalTest
{
protected static $fixture_file = 'CSVImportTest.yml';
protected static $use_draft_site = true;


public function test_csv_import_task()
{
//GIVEN we have a csv file with data to import
//WHEN we run the import task
$response = $this->get('dev/tasks/CSVImportTask');
//THEN the Customers Table should be have 1000 records
$this->assertEquals(1000, Customer::get()->count());
// THEN the Third Record form the CSV should match up with the Third Customers record in the DB

$third = Customer::get()->byID(3);
$this->assertEquals('Craig', $third->FirstName);
$this->assertEquals('Mccoy', $third->LastName);
$this->assertEquals('cmccoy2@bluehost.com', $third->Email);
$this->assertEquals('Male', $third->Gender);
$this->assertEquals('75.162.167.180', $third->IPAddress);
$this->assertEquals('Quatz', $third->Company);
$this->assertEquals('Srpska Crnja', $third->City);
$this->assertEquals('Senior Sales Associate', $third->Title);
$this->assertEquals('https://cdc.gov/iaculis.png?vulputate=sapien&justo=varius&in=ut&blandit=blandit&ultrices=non&enim=interdum&lorem=in&ipsum=ante', $third->Website);

// WHEN we run the task again
$response = $this->get('dev/tasks/CSVImportTask');
// THEN no more Customers should be added
$this->assertEquals(1000, Customer::get()->count());
}
}
Empty file added app/tests/CSVImportTest.yml
Empty file.
35 changes: 35 additions & 0 deletions app/tests/CustomerJSONEndpointTest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Page:
One:
"URLSegment" : "home"

sasky\catchTest\app\Customer:
One:
"FirstName": "Terry"
"LastName": "Ruiz"
"Email": "truiz5@unc.edu"
"Gender": "Male"
"IPAddress": "188.77.133.124"
"Company": "Nlounge"
"City": "Souflí"
"Title": ""
"Website": ""
Two:
"FirstName": "Lawrence"
"LastName": "West"
"Email": "lwestc@1688.com"
"Gender": "Male"
"IPAddress": "109.133.189.108"
"Company": "Gabcube"
"City": "Coayllo"
"Title": "Electrical Engineer"
"Website": "http://symantec.com/in.js?sapien=magnis&sapien=dis&non=parturient&mi=montes&integer=nascetur&ac=ridiculus&neque=mus&duis=etiam&bibendum=vel&morbi=augue&non=vestibulum&quam=rutrum&nec=rutrum"
Three:
"FirstName": "Nicholas"
"LastName": "Hart"
"Email": "nharti@oakley.com"
"Gender": "Male"
"IPAddress": "182.5.26.33"
"Company": "Riffpath"
"City": "Santa Cruz De Tenerife"
"Title": "Recruiting Manager"
"Website": "https://japanpost.jp/quam/turpis/adipiscing/lorem/vitae.js?primis=consequat&in=metus&faucibus=sapien&orci=ut&luctus=nunc&et=vestibulum&ultrices=ante&posuere=ipsum&cubilia=primis&curae=in&duis=faucibus&faucibus=orci&accumsan=luctus&odio=et&curabitur=ultrices&convallis=posuere&duis=cubilia&consequat=curae&dui=mauris&nec=viverra&nisi=diam&volutpat=vitae&eleifend=quam&donec=suspendisse&ut=potenti&dolor=nullam&morbi=porttitor&vel=lacus&lectus=at&in=turpis&quam=donec&fringilla=posuere"
Loading