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
23 changes: 23 additions & 0 deletions .docker/dockerfile_php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM php:8.3-fpm

#install basic packages
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y git zip unzip iputils-ping traceroute net-tools netbase nano rsync sudo

# install composer
RUN php -r "copy('http://getcomposer.org/installer', 'composer-setup.php');"
RUN php composer-setup.php
RUN php -r "unlink('composer-setup.php');"
RUN mv composer.phar /usr/local/bin/composer

#fetch the install php packages wrapper
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions

#install additional extensions
RUN install-php-extensions gd imagick/imagick@master xdebug imap opcache soap zip intl mcrypt xmlrpc exif mysqli pdo pdo_mysql pdo_sqlite sqlite3 mbstring bcmath ldap curl

# force the env to use unlimited memory with the docker container
RUN cd /usr/local/etc/php/conf.d/ && echo 'memory_limit = -1' >> /usr/local/etc/php/conf.d/docker-php-memlimit.ini

2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

REQRES_API_KEY=
87 changes: 87 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
ENV_FILE_PATH := ./.env

ifeq ($(wildcard $(ENV_FILE_PATH)),)
$(error .env file is missing. Please ensure the file has been copied from : ./.env.dist)
endif

# Now include the .env file
include $(ENV_FILE_PATH)

# Export all variables from the .env files
export $(shell sed 's/=.*//' $(ENV_FILE_PATH))

# Executables (local)
DOCKER_APP_EXEC = docker compose exec -it php


## —— Bashes ————————————————————————————————————————————————————————————

bash:
@$(DOCKER_APP_EXEC) bash

build:
@docker compose build

up:
@docker compose up

down:
@docker compose down

## -- Build ----------------------------------------------------------------

composer-install:
@$(DOCKER_APP_EXEC) composer install

composer-update:
@$(DOCKER_APP_EXEC) composer update

composer-dump-autoload:
@$(DOCKER_APP_EXEC) composer dump-autoload


## -- Fixers and validators ----------------------------------------------------------------

fixers:
@$(DOCKER_APP_EXEC) ./vendor/bin/pint app --config laravel.pint.json || true

validate:
@echo "Running PHP Mess Detector"
@$(DOCKER_APP_EXEC) ./vendor/bin/phpmd app text ./phpmd.rules.xml --color

@echo "Running PHP Code Sniffer"
@${DOCKER_APP_EXEC} ./vendor/bin/phpcs app -p --colors --warning-severity=0 --report=code --standard=./phpcs.dist.xml -s

@echo "Clearing PHP Stan cache"
@${DOCKER_APP_EXEC} ./vendor/bin/phpstan clear-result-cache --memory-limit=1G --configuration ./phpstan.neon

@echo "Running PHP Stan analysis"
@${DOCKER_APP_EXEC} ./vendor/bin/phpstan analyse --memory-limit=1G --configuration ./phpstan.neon --error-format=table

validate-md:
@echo "Running PHP Mess Detector"
@$(DOCKER_APP_EXEC) ./vendor/bin/phpmd app text ./phpmd.rules.xml --color --baseline-file=./phpmd.baseline.xml

validate-cs:
@echo "Running PHP Code Sniffer"
@${DOCKER_APP_EXEC} ./vendor/bin/phpcs app -p --colors --warning-severity=0 --report=code --standard=./phpcs.dist.xml -s

validate-stan:
@echo "Clearing PHP Stan cache"
@${DOCKER_APP_EXEC} ./vendor/bin/phpstan clear-result-cache --memory-limit=1G --configuration ./phpstan.neon
@echo "Running PHP Stan analysis"
@${DOCKER_APP_EXEC} ./vendor/bin/phpstan analyse --memory-limit=1G --configuration ./phpstan.neon --error-format=table

## -- Unit Testing ----------------------------------------------------------------

.PHONY: tests
tests:
@make --no-print-directory tests-delete-reports
@$(DOCKER_APP_EXEC) bash -c 'export XDEBUG_MODE=coverage && export APP_ENV=testing && ./vendor/bin/paratest \
--configuration ./tests/phpunit.xml \
--coverage-clover ./tests/reports/phpunit.coverage.xml \
--coverage-html tests/reports/unit/html/ \
--coverage-text=tests/reports/unit/phpunit.coverage.txt \
--log-junit tests/reports/unit/phpunit.junit.xml \
--processes=$(nproc)'

53 changes: 53 additions & 0 deletions Nextsteps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

Further coding bits that may help this solution:

## Failures

- upon API failures when running the code what is the plan? Notification going over to dev+ops teams, logging, supplier call-out?
- upon failure, would the db hold up on a rollback? Should each upsert be its own transaction or will a rollback create so many locks that it would impact regular usage?

## Data ingested

- the data added to the Users need to be analysed from the point of indexes to make sure that anything being queried is properly fetched using as much indexing as possible/usable/required. Over-indexing should also be taken into consideration if the User table has too many fields.
- if the amount of data being updated/inserted is too much a bulk execution should be considered or even a downtime window

## Unit tests

- unit tests with mocked endpoints for the service (Http::fake)
-- run this in the regular pipeline
- unit tests in a separate suite that will hit the actual API service to validate it and manage its uptime
-- this should be outside of the regular pipeline to avoid snags and API outages
-- alerts/notifications should be in place for this to make sure observability of the service is high depending (slack, email, etc...)
- unit test would be:
-- no pages defined to fetch
-- all pages to fetch
-- specific page number to fetch
--

## UseCase

- if the specs require usage of the same functionality that the Command has of upserting the User then the code can be moved to a UseCase. This way the functionality can be used by any other UseCase, Command, Controller, etc... when needed.

## Audit trail

- if observability is needed, add a log of the updates that are done (log file only)
-- if further audit is needed a timestamp change table can be created to log any change done to the User record (a poly table would be ideal so other records can benefit from it)
-- if audit code starts cluttering too much then it can be moved to an Observer

## Docs

- at least a high-level, non-techinical description doc explaining what goes on in this code and details for it like scheduling, requirements, 3rd party doc links, etc...

## Supplier info

- the API may fail if enough calls are made so throtling may need to be implemented (avoids blacklisting, IP delaying, etc...). This needs to be checked with the supplier to make sure no limits are crossed.

## Deployment

- devops notifications and awareness - since this will run hourly they need to know what will create processing spikes in the box and db

## Logging

- when the process end, what should happen? Any specifics into who needs to be notified or what systems need to have this information (Cloudwatch, Grafana, Datadog, etc...)


77 changes: 77 additions & 0 deletions app/Console/Commands/FetchReqresUsers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Services\ReqresApiService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

class FetchReqresUsers extends Command
{
protected $signature = 'reqres:fetch-users {--page=1 : The page number to fetch} {--fetch-all : Fetch all available pages}';

protected $description = 'Fetch users from Reqres API and store them in the database';

private ReqresApiService $reqresService;

public function __construct(ReqresApiService $reqresService)
{
parent::__construct();
$this->reqresService = $reqresService;
}

private function processUsersFromResponse(array $response): void
{
foreach ($response['data'] as $userData) {
User::updateOrCreate(
['email' => $userData['email']],
[
'name' => $userData['first_name'] . ' ' . $userData['last_name'],
'reqres_id' => $userData['id'],
'password' => bcrypt('password'),
]
);
}
}

public function handle()
{
try {
if ($this->option('fetch-all')) {
$response = $this->reqresService->getUsers(1);
$totalPages = $response['total_pages'] ?? 1;

$this->info("Fetching all pages (total: {$totalPages})");

$this->info('Processing page 1 of users');
$this->processUsersFromResponse($response);

for ($page = 2; $page <= $totalPages; $page++) {
$response = $this->reqresService->getUsers($page);
$this->info("Processing page {$page} of users");
$this->processUsersFromResponse($response);
}

$this->info('Completed fetching all pages');
} else {
$page = (int) $this->option('page');
$response = $this->reqresService->getUsers($page);
$this->info("Processing page {$page} of users");
$this->processUsersFromResponse($response);
$this->info("Completed fetching page {$page} of users");
}
} catch (\Exception $e) {
Log::error('Failed to fetch users from Reqres API', [
'error' => $e->getMessage(),
'fetch_all' => $this->option('fetch-all'),
'page' => $this->option('page'),
]);
$this->error($e->getMessage());

return 1;
}

return 0;
}
}
5 changes: 2 additions & 3 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ class Kernel extends ConsoleKernel
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
$schedule->command('reqres:fetch-users --page=1')->hourly();
}

/**
Expand All @@ -34,7 +33,7 @@ protected function schedule(Schedule $schedule)
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
$this->load(__DIR__ . '/Commands');

require base_path('routes/console.php');
}
Expand Down
4 changes: 3 additions & 1 deletion app/Http/Controllers/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@

class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
}
2 changes: 0 additions & 2 deletions app/Http/Middleware/RedirectIfAuthenticated.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ class RedirectIfAuthenticated
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null ...$guards
* @return mixed
*/
Expand Down
11 changes: 6 additions & 5 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,31 @@

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
use HasFactory, Notifiable;
use HasFactory;
use Notifiable;

/**
* The attributes that are mass assignable.
*
* @var array
* @var array<string>
*/
protected $fillable = [
'name',
'email',
'password',
'reqres_id',
];

/**
* The attributes that should be hidden for arrays.
*
* @var array
* @var array<string>
*/
protected $hidden = [
'password',
Expand All @@ -35,7 +36,7 @@ class User extends Authenticatable
/**
* The attributes that should be cast to native types.
*
* @var array
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
Expand Down
1 change: 0 additions & 1 deletion app/Providers/AuthServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
Expand Down
35 changes: 35 additions & 0 deletions app/Services/ReqresApiService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use RuntimeException;

class ReqresApiService
{
private string $baseUrl = 'https://reqres.in/api';

private array $headers;

public function __construct()
{
$this->headers = [
'x-api-key' => env('REQRES_API_KEY'),
'Accept' => 'application/json',
];
}

public function getUsers(int $page = 1): array
{
$response = Http::withHeaders($this->headers)
->get("{$this->baseUrl}/users", [
'page' => $page,
]);

if (! $response->successful()) {
throw new RuntimeException('Failed to fetch users from Reqres API: ' . $response->body());
}

return $response->json();
}
}
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
"laravel/framework": "^8.40"
},
"require-dev": {
"brianium/paratest": "^6.11",
"facade/ignition": "^2.5",
"fakerphp/faker": "^1.9.1",
"larastan/larastan": "^1.0",
"laravel/pint": "^1.25",
"mockery/mockery": "^1.4.2",
"nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^9.3.3"
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^4.0"
},
"autoload": {
"psr-4": {
Expand Down
Loading