erreur imcopréhenssible
continuous-integration/drone/push Build is failing Details

issue_022_AjoutAmis
David D'ALMEIDA 1 year ago
parent eb625309ce
commit 48e6c4877d

@ -38,7 +38,7 @@
"phpunit/phpunit": "*" "phpunit/phpunit": "*"
}, },
"scripts": { "scripts": {
"dev": "php -S localhost:8080 -t public -d display_errors=1 -d error_reporting=E_ALL", "dev": "php -S localhost:8081 -t public -d display_errors=1 -d error_reporting=E_ALL",
"dev:console": "export APP_ENV=console && php public/index.php", "dev:console": "export APP_ENV=console && php public/index.php",
"dev:html" : "export APP_ENV=html && php -S localhost:8080 -t public -d display_errors=1 -d error_reporting=E_ALL" "dev:html" : "export APP_ENV=html && php -S localhost:8080 -t public -d display_errors=1 -d error_reporting=E_ALL"
} }

2320
Sources/composer.lock generated

File diff suppressed because it is too large Load Diff

@ -6,14 +6,54 @@ require_once __DIR__ . '/../config/config.php';
use App\AppCreator; use App\AppCreator;
use App\Router\Middleware\LoggingMiddleware; use App\Router\Middleware\LoggingMiddleware;
use App\Router\Request\RequestFactory; use App\Router\Request\RequestFactory;
use Manager\UserManager;
use Manager\DataManager;
use Repository\IUserRepository;
use Shared\ArgumentControllerResolver; use Shared\ArgumentControllerResolver;
use Shared\IArgumentResolver; use Shared\IArgumentResolver;
use Stub\AuthService;
use Stub\NotificationRepository;
use Stub\TrainingRepository;
use Stub\UserRepository;
use Stub\RelationshipRequestRepository;
use Repository\INotificationRepository;
use App\Router\Middleware\AuthMiddleware;
use Network\IAuthService;
use Network\IFriendRequestService;
use Network\RelationshipService;
use Network\INotificationService;
use Stub\NotificationService;
use Stub\StubData;
use Twig\Environment; use Twig\Environment;
use Twig\Loader\FilesystemLoader; use Twig\Loader\FilesystemLoader;
use Shared\Log; use Shared\IHashPassword;
use Shared\HashPassword;
$appFactory = new AppCreator(); $appFactory = new AppCreator();
$appFactory->registerService(IArgumentResolver::class, ArgumentControllerResolver::class); $appFactory->registerService(IArgumentResolver::class, ArgumentControllerResolver::class);
$appFactory->registerService(UserManager::class, UserManager::class);
$appFactory->registerService(DataManager::class, StubData::class);
$appFactory->registerService(IAuthService::class, AuthService::class);
$appFactory->registerService(IFriendRequestService::class, RelationshipService::class);
$appFactory->registerService(IHashPassword::class, HashPassword::class);
$appFactory->registerService(INotificationService::class, NotificationService::class);
$appFactory->registerService(INotificationRepository::class, NotificationRepository::class);
$appFactory->registerService(IUserRepository::class, UserRepository::class);
$appFactory->registerService(\Twig\Loader\LoaderInterface::class, function() { $appFactory->registerService(\Twig\Loader\LoaderInterface::class, function() {
return new FilesystemLoader(__DIR__ . '/../src/app/views/Templates'); return new FilesystemLoader(__DIR__ . '/../src/app/views/Templates');
@ -29,7 +69,7 @@ $app = $appFactory->create();
if (!is_null($app)){ if (!is_null($app)){
// Ajout des Middleware // Ajout des Middleware
/*$app->use(new LoggingMiddleware());*/ /*$app->use(new LoggingMiddleware());*/
$app->use(new AuthMiddleware());
$app->mapControllers(); $app->mapControllers();
$app->run(RequestFactory::createFromGlobals()); $app->run(RequestFactory::createFromGlobals());
} }

@ -29,6 +29,9 @@ class App
private FrontController $frontController; private FrontController $frontController;
private Session $session;
public function __construct(string $appName, int $version, \App\Container $diContainer) public function __construct(string $appName, int $version, \App\Container $diContainer)
{ {
$this->appName = $appName; $this->appName = $appName;
@ -36,6 +39,7 @@ class App
$this->container = $diContainer; $this->container = $diContainer;
$this->router = new Router(""); $this->router = new Router("");
$this->frontController = new FrontController($this->router,$this->container); $this->frontController = new FrontController($this->router,$this->container);
$this->session = Session::getInstance();
} }
public function use(IHttpMiddleware $middleware) public function use(IHttpMiddleware $middleware)

@ -17,7 +17,6 @@ class Container implements ContainerInterface
if ($this->has($id)) { if ($this->has($id)) {
$entry = $this->entries[$id]; $entry = $this->entries[$id];
if (is_callable($entry)) { if (is_callable($entry)) {
return $entry($this); return $entry($this);
} }

@ -1,177 +1,176 @@
<?php <?php
// namespace App\Controller; namespace App\Controller;
// use App\Container; use App\Container;
// use App\Router\Request\IRequest; use App\Router\Request\IRequest;
// use App\Router\Response\Response; use App\Router\Response\Response;
// use Shared\Attributes\Route; use Shared\Attributes\Route;
// use Twig\Environment; use Twig\Environment;
// use Data\Core\Preferences; use Data\Core\Preferences;
// use Shared\Log; use Shared\Log;
// class AthleteController extends BaseController class AthleteController extends BaseController
// { {
// #[Route(path: '/search-user', name: 'search-user', methods: ['GET'])] #[Route(path: '/search-ath', name: 'search-ath', methods: ['GET'])]
// public function searchUser(string $username, IRequest $req): Response public function searchUser(string $username, IRequest $req): Response
// { {
// $taberror = []; $taberror = [];
// $utiliArray = [ $utiliArray = [
// [ [
// 'nom' => 'John', 'nom' => 'John',
// 'prenom' => 'Doe', 'prenom' => 'Doe',
// 'img' => 'john_doe', 'img' => 'john_doe',
// 'username' => 'johndoe', 'username' => 'johndoe',
// ], ],
// [ [
// 'nom' => 'Alice', 'nom' => 'Alice',
// 'prenom' => 'Smith', 'prenom' => 'Smith',
// 'img' => 'alice_smith', 'img' => 'alice_smith',
// 'username' => 'alicesmith', 'username' => 'alicesmith',
// ], ],
// ]; ];
// // if(!Validation::val_string($name)){ // if(!Validation::val_string($name)){
// try { try {
// //code... //code...
// // $model->userMgr->getUser($name); // $model->userMgr->getUser($name);
// return $this->render('./page/addfriend.html.twig',[ return $this->render('./page/addfriend.html.twig', [
// 'css' => $this->preference->getCookie(), 'css' => $this->preference->getCookie(),
// 'pp' => "test2", 'pp' => "test2",
// 'user' => "Doe", 'user' => "Doe",
// 'role' => "Athlète", 'role' => "Athlète",
// 'friendship' => [], 'friendship' => [],
// 'analyzes' => [], 'analyzes' => [],
// 'mails' => [], 'mails' => [],
// 'users' => $utiliArray, 'users' => $utiliArray,
// 'infoUser' => [], 'infoUser' => [],
// 'exos' => [], 'exos' => [],
// 'member' => [], 'member' => [],
// 'responce' => "Notification d'ajout envoyée à $username" 'responce' => "Notification d'ajout envoyée à $username"
// ]); ]);
// } catch (\Throwable $th) { } catch (\Throwable $th) {
// //throw $th; //throw $th;
// // return $this->render("addfriend.html.twig", ['tabError' => $taberror ]); // return $this->render("addfriend.html.twig", ['tabError' => $taberror ]);
// } }
// // } // }
// } }
// #[Route(path: '/analyses', name: 'analyses', methods: ['GET'])] #[Route(path: '/analyses', name: 'analyses', methods: ['GET'])]
// public function analyses(): Response public function analyses(): Response
// { {
// return $this->render('./page/analyze.html.twig',[ return $this->render('./page/analyze.html.twig', [
// 'css' => $this->preference->getCookie(), 'css' => $this->preference->getCookie(),
// 'pp' => "test2", 'pp' => "test2",
// 'user' => "Doe", 'user' => "Doe",
// 'role' => "Athlète", 'role' => "Athlète",
// 'friendship' => [], 'friendship' => [],
// 'analyzes' => [], 'analyzes' => [],
// 'mails' => [], 'mails' => [],
// 'users' => [], 'users' => [],
// 'infoUser' => [], 'infoUser' => [],
// 'exos' => [], 'exos' => [],
// 'member' => [] 'member' => []
// ]); ]);
// } }
// #[Route(path: '/exercice', name: 'exercice', methods: ['GET'])] // 8 #[Route(path: '/exercice', name: 'exercice', methods: ['GET'])] // 8
// public function exercice(): Response public function exercice(): Response
// { {
// return $this->render('./page/exercice.html.twig',[ return $this->render('./page/exercice.html.twig', [
// 'css' => $this->preference->getCookie(), 'css' => $this->preference->getCookie(),
// 'pp' => "test2", 'pp' => "test2",
// 'user' => "Doe", 'user' => "Doe",
// 'role' => "Athlète", 'role' => "Athlète",
// 'friendship' => [], 'friendship' => [],
// 'analyzes' => [], 'analyzes' => [],
// 'mails' => [], 'mails' => [],
// 'users' => [], 'users' => [],
// 'infoUser' => [], 'infoUser' => [],
// 'exos' => [], 'exos' => [],
// 'member' => [] 'member' => []
// ]); ]);
// } }
// #[Route(path: '/add-friend', name: 'add-friend', methods: ['POST'])] #[Route(path: '/add-friend', name: 'add-friend', methods: ['POST'])]
// public function addFriend(string $username, IRequest $req): Response public function addFriend(string $username, IRequest $req): Response
// { {
// $taberror = []; $taberror = [];
// $utiliArray = [ $utiliArray = [
// [ [
// 'nom' => 'John', 'nom' => 'John',
// 'prenom' => 'Doe', 'prenom' => 'Doe',
// 'img' => 'john_doe', 'img' => 'john_doe',
// 'username' => 'johndoe', 'username' => 'johndoe',
// ], ],
// [ [
// 'nom' => 'Alice', 'nom' => 'Alice',
// 'prenom' => 'Smith', 'prenom' => 'Smith',
// 'img' => 'alice_smith', 'img' => 'alice_smith',
// 'username' => 'alicesmith', 'username' => 'alicesmith',
// ], ],
// ]; ];
// // if(!Validation::val_string($name)){ // if(!Validation::val_string($name)){
// try { try {
// //code... //code...
// // $model->userMgr->addFriend($name); // $model->userMgr->addFriend($name);
// return $this->render('./page/addfriend.html.twig',[ return $this->render('./page/addfriend.html.twig', [
// 'css' => $this->preference->getCookie(), 'css' => $this->preference->getCookie(),
// 'pp' => "test2", 'pp' => "test2",
// 'user' => "Doe", 'user' => "Doe",
// 'role' => "Athlète", 'role' => "Athlète",
// 'friendship' => [], 'friendship' => [],
// 'analyzes' => [], 'analyzes' => [],
// 'mails' => [], 'mails' => [],
// 'users' => $utiliArray, 'users' => $utiliArray,
// 'infoUser' => [], 'infoUser' => [],
// 'exos' => [], 'exos' => [],
// 'member' => [], 'member' => [],
// 'responce' => "Notification d'ajout envoyée à $username" 'responce' => "Notification d'ajout envoyée à $username"
// ]); ]);
// } catch (\Throwable $th) { } catch (\Throwable $th) {
// //throw $th; //throw $th;
// // return $this->render("addfriend.html.twig", ['tabError' => $taberror ]); // return $this->render("addfriend.html.twig", ['tabError' => $taberror ]);
// } }
// // } // }
// } }
// #[Route(path: '/delete-friend', name: 'delete-friend', methods: ['POST'])]
// #[Route(path: '/friend', name: 'friend', methods: ['GET'])]
// public function friend(): Response
// {
// $utiliArray = [
// [
// 'nom' => 'John',
// 'prenom' => 'Doe',
// 'img' => 'john_doe',
// 'username' => 'johndoe',
// ],
// [
// 'nom' => 'Alice',
// 'prenom' => 'Smith',
// 'img' => 'alice_smith',
// 'username' => 'alicesmith',
// ],
// ];
// // $this->Auth->getUser->role->getFriends
// return $this->render('./page/addfriend.html.twig',[
// 'css' => $this->preference->getCookie(),
// 'pp' => "test2",
// 'user' => "Doe",
// 'role' => "Athlète",
// 'friendship' => [],
// 'analyzes' => [],
// 'mails' => [],
// 'users' => $utiliArray,
// 'infoUser' => [],
// 'exos' => [],
// 'member' => [],
// ]);
// }
// } #[Route(path: '/friend', name: 'friend', methods: ['GET'])]
public function friend(): Response
{
$utiliArray = [
[
'nom' => 'John',
'prenom' => 'Doe',
'img' => 'john_doe',
'username' => 'johndoe',
],
[
'nom' => 'Alice',
'prenom' => 'Smith',
'img' => 'alice_smith',
'username' => 'alicesmith',
],
];
return $this->render('./page/addfriend.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => $utiliArray,
'infoUser' => [],
'exos' => [],
'member' => [],
]);
}
}

@ -1,80 +1,148 @@
<?php <?php
// namespace App\Controller; namespace App\Controller;
// use App\Container; use App\Container;
// use App\Router\Request\IRequest; use App\Router\Request\IRequest;
// use App\Router\Response\Response; use App\Router\Response\Response;
// use Shared\Attributes\Route; use App\Router\Response\IResponse;
// use Twig\Environment;
// use Data\Core\Preferences; use Manager\UserManager;
// use Shared\Log; use Shared\Attributes\Route;
use Shared\Validation;
// class AuthController extends BaseController use Twig\Environment;
// { use Data\Core\Preferences;
// #[Route('/login', name: 'login')] use Shared\Log;
// public function login(?string $username, ?string $password ,IRequest $request): Response {
// // if user is already logged in, don't display the login page again class AuthController extends BaseController
// if ($user) { {
// return $this->redirectToRoute('blog_index');
// }
#[Route('/login', name: 'login',methods: ['POST'])]
// // this statement solves an edge-case: if you change the locale in the login public function login(IRequest $request): IResponse {
// // page, after a successful login you are redirected to a page in the previous
// // locale. This code regenerates the referrer URL whenever the login page is $error = [];
// // browsed, to ensure that its locale is always the current one. try {
// $this->saveTargetPath($request->getSession(), 'main', $this->generateUrl('admin_index')); $log=Validation::clean_string($request->getBody()['email']);
$mdp=Validation::clean_string($request->getBody()['password']);
// return $this->render('security/login.html.twig', [ } catch (\Throwable $th) {
// // last username entered by the user (if any) $error = "Wrong cred";
// 'last_username' => $helper->getLastUsername(), }
// // last authentication error (if any)
// 'error' => $helper->getLastAuthenticationError(), if($this->container->get(UserManager::class)->login($log,$mdp)){
// ]); return $this->redirectToRoute('/');
// } }
else{
// #[Route('/login', name: 'login')] $error [] = "Erreur de connexion. Essayez encore";
// public function login(?string $username, ?string $password ,IRequest $request): Response { }
// // if user is already logged in, don't display the login page again return $this->render('./page/login.html.twig', ['error' => $error]);
// if ($user) {
// return $this->redirectToRoute('blog_index');
// }
}
// // this statement solves an edge-case: if you change the locale in the login
// // page, after a successful login you are redirected to a page in the previous #[Route('/log', name: 'baseLog',methods: ['GET'])]
// // locale. This code regenerates the referrer URL whenever the login page is public function index(IRequest $request): IResponse {
// // browsed, to ensure that its locale is always the current one.
// $this->saveTargetPath($request->getSession(), 'main', $this->generateUrl('admin_index')); return $this->render('./page/login.html.twig',[
'css' => $this->preference->getCookie(),
// return $this->render('security/login.html.twig', [ 'pp' => "test2",
// // last username entered by the user (if any) 'user' => "Doe",
// 'last_username' => $helper->getLastUsername(), 'role' => "Athlète",
// // last authentication error (if any) 'friendship' => [],
// 'error' => $helper->getLastAuthenticationError(), 'analyzes' => [],
// ]); 'mails' => [],
// } 'users' => [],
'infoUser' => [],
'exos' => [],
// function inscription() { 'member' => []
// $model = new ModelVisitor(); ]);
// $log=Validation::clean_string($_POST['pseudo']);
// $mdp=Validation::clean_string($_POST['password']); }
// if($model->createAUser($log,$mdp)){
// if(ModelUser::login($log, $mdp)){ #[Route('/register', name: 'register' , methods:['GET'])]
// UserControler::displayView(); public function register(IRequest $request): IResponse
// } {
// } if ($request->getMethod() == 'POST') {
// } $nom = $request->getBody()['nom'];
// function login() {
// $model = new ModelVisitor(); $prenom = $request->getBody()['prenom'];
// if(!isset($_POST['pseudo']) || !isset($_POST['password'])) throw new Exception(" some wrong with credentials !!!!!");
// $log=Validation::clean_string($_POST['pseudo']); $username = $request->getBody()['username'];
// $mdp=Validation::clean_string($_POST['password']);
// if(ModelUser::login($log, $mdp)){ $email = $request->getBody()['email'];
// UserControler::displayView();
// } $motDePasse = $request->getBody()['motDePasse'];
// } $sexe = $request->getBody()['sexe'];
$taille = $request->getBody()['taille'];
// }
$poids = $request->getBody()['poids'];
$dateNaissanceStr = $request->getBody()['nom'];
$dateNaissance = new \DateTime($dateNaissanceStr);
if (!$dateNaissance) {
throw new \Exception("Date de naissance non valide. Format attendu : YYYY-MM-DD");
}
$roleName = $request->getBody()['roleName'];
$registrationData = [
'nom' => $nom,
'prenom' => $prenom,
'username' => $username,
'email' => $email,
'sexe' => $sexe,
'taille' => $taille,
'poids' => $poids,
'dateNaissance' => $dateNaissance,
'roleName' => $roleName
];
try {
if ($this->container->get(UserManager::class)->register($email, $motDePasse, $registrationData)) {
return $this->redirectToRoute('/');
} else {
$error [] = 'L\'inscription a échoué. Veuillez réessayer.';
}
} catch (\Exception $e) {
$error [] = 'Erreur lors de l\'inscription: ' . $e->getMessage();
}
}
return $this->render('/register.html.twig');
}
#[Route(path: '/mdp', name: 'mdp', methods: ['POST'])]
public function mdp(string $ancienMotDePasse,string $nouveauMotDePasse,string $confirmerMotDePasse, IRequest $req): Response
{
// CONFIRMER LES DONNESS !!!!! IMPORTANT
return $this->render('./page/settings.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
}

@ -1,6 +1,7 @@
<?php <?php
namespace App\Controller; namespace App\Controller;
use Data\Core\Preferences;
use App\Container; use App\Container;
use App\Router\Response\RedirectResponse; use App\Router\Response\RedirectResponse;
@ -10,6 +11,12 @@ use Psr\Container\ContainerInterface;
abstract class BaseController abstract class BaseController
{ {
protected Preferences $preference;
public function __construct(){
$this->preference = new Preferences();
}
protected ContainerInterface $container; protected ContainerInterface $container;
public function setContainer(ContainerInterface $container) public function setContainer(ContainerInterface $container)

@ -13,50 +13,6 @@ use Shared\Log;
// TODO : Remove this BaseClass // TODO : Remove this BaseClass
class Controller extends BaseController class Controller extends BaseController
{ {
private Environment $twig;
private Preferences $preference;
public function __construct()
{
session_start();
$this->preference = new Preferences();
}
#[Route(path: '/', name: 'home', methods: ['GET'])]
public function index(): Response
{
return $this->render('./page/home.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
#[Route(path: '/analyses', name: 'analyses', methods: ['GET'])]
public function analyses(): Response
{
return $this->render('./page/analyze.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
#[Route(path: '/activity', name: 'activity', methods: ['GET'])] #[Route(path: '/activity', name: 'activity', methods: ['GET'])]
public function activity(): Response public function activity(): Response
@ -76,23 +32,6 @@ class Controller extends BaseController
]); ]);
} }
#[Route(path: '/exercice', name: 'exercice', methods: ['GET'])] // 8
public function exercice(): Response
{
return $this->render('./page/exercice.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
#[Route(path: '/exercices', name: 'exercices', methods: ['POST'])] // 8 #[Route(path: '/exercices', name: 'exercices', methods: ['POST'])] // 8
public function exercices(String $type, String $intensite, String $date, IRequest $req): Response public function exercices(String $type, String $intensite, String $date, IRequest $req): Response
@ -286,81 +225,6 @@ class Controller extends BaseController
]); ]);
} }
#[Route(path: '/add-friend', name: 'add-friend', methods: ['POST'])]
public function addFriend(string $username, IRequest $req): Response
{
$taberror = [];
$utiliArray = [
[
'nom' => 'John',
'prenom' => 'Doe',
'img' => 'john_doe',
'username' => 'johndoe',
],
[
'nom' => 'Alice',
'prenom' => 'Smith',
'img' => 'alice_smith',
'username' => 'alicesmith',
],
];
// if(!Validation::val_string($name)){
try {
//code...
// $model->userMgr->addFriend($name);
return $this->render('./page/addfriend.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => $utiliArray,
'infoUser' => [],
'exos' => [],
'member' => [],
'responce' => "Notification d'ajout envoyée à $username"
]);
} catch (\Throwable $th) {
//throw $th;
// return $this->render("addfriend.html.twig", ['tabError' => $taberror ]);
}
// }
}
#[Route(path: '/friend', name: 'friend', methods: ['GET'])]
public function friend(): Response
{
$utiliArray = [
[
'nom' => 'John',
'prenom' => 'Doe',
'img' => 'john_doe',
'username' => 'johndoe',
],
[
'nom' => 'Alice',
'prenom' => 'Smith',
'img' => 'alice_smith',
'username' => 'alicesmith',
],
];
return $this->render('./page/addfriend.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => $utiliArray,
'infoUser' => [],
'exos' => [],
'member' => [],
]);
}
#[Route(path: '/friendlist', name: 'friendlist', methods: ['POST'])] #[Route(path: '/friendlist', name: 'friendlist', methods: ['POST'])]
public function friendlist(string $username, IRequest $req): Response public function friendlist(string $username, IRequest $req): Response
@ -486,23 +350,6 @@ class Controller extends BaseController
]); ]);
} }
#[Route(path: '/settings', name: 'settings', methods: ['GET'])]
public function settings(IRequest $req): Response
{
return $this->render('./page/settings.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
#[Route(path: '/profile', name: 'profile', methods: ['GET'])] #[Route(path: '/profile', name: 'profile', methods: ['GET'])]
public function profile(): Response public function profile(): Response
@ -522,28 +369,6 @@ class Controller extends BaseController
]); ]);
} }
#[Route(path: '/preferences', name: 'preferences', methods: ['POST'])]
public function preferences(string $theme, IRequest $req): Response
{
/*TODO*/
// VALIDER LES DONNEES
$this->preference->majCookie($theme);
return $this->render('./page/settings.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
#[Route(path: '/psettings', name: 'psettings', methods: ['POST'])] #[Route(path: '/psettings', name: 'psettings', methods: ['POST'])]
public function psettings(string $nom,string $prenom,string $dateNaissance,string $mail,string $tel, IRequest $req): Response public function psettings(string $nom,string $prenom,string $dateNaissance,string $mail,string $tel, IRequest $req): Response
@ -565,26 +390,6 @@ class Controller extends BaseController
]); ]);
} }
#[Route(path: '/mdp', name: 'mdp', methods: ['POST'])]
public function mdp(string $ancienMotDePasse,string $nouveauMotDePasse,string $confirmerMotDePasse, IRequest $req): Response
{
// CONFIRMER LES DONNESS !!!!! IMPORTANT
return $this->render('./page/settings.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
} }

@ -47,6 +47,9 @@ class FrontController {
} catch (NotFoundHttpException $e) { } catch (NotFoundHttpException $e) {
$this->handleError(404, $e->getMessage()); $this->handleError(404, $e->getMessage());
} }
catch(\Throwable $e){
$this->handleError(501, $e->getMessage());
}
} }
private function getController($controllerSpec) { private function getController($controllerSpec) {

@ -1,17 +1,19 @@
<!-- #[Route(path: '/mail', name: 'mail', methods: ['GET'])] <?php
public function mail(): Response
{ // namespace App\Controller;
return $this->render('./page/mail.html.twig',[
'css' => $this->preference->getCookie(), // use App\Container;
'pp' => "test2", // use App\Router\Request\IRequest;
'user' => "Doe", // use App\Router\Response\Response;
'role' => "Athlète", // use App\Router\Response\IResponse;
'friendship' => [],
'analyzes' => [], // use Shared\Attributes\Route;
'mails' => [], // use Twig\Environment;
'users' => [], // use Data\Core\Preferences;
'infoUser' => [], // use Shared\Log;
'exos' => [],
'member' => []
]);
} --> // #[Route(path: '/coach', name: 'coach')]
// class CoachController extends BaseController
// {

@ -1,80 +1,78 @@
<?php <?php
// namespace App\Controller; namespace App\Controller;
// use App\Container; use App\Container;
// use App\Router\Request\IRequest; use App\Router\Request\IRequest;
// use App\Router\Response\Response; use App\Router\Response\Response;
// use Shared\Attributes\Route; use Shared\Attributes\Route;
// use Twig\Environment; use Twig\Environment;
// use Data\Core\Preferences; use Data\Core\Preferences;
// use Shared\Log; use Shared\Log;
// class UserController extends BaseController class UserController extends BaseController
// { {
#[Route(path: '/', name: 'home', methods: ['GET'])]
public function index(): Response
{
return $this->render('./page/home.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
// #[Route(path: '/', name: 'home', methods: ['GET'])]
// public function index(): Response
// {
// return $this->render('./page/home.html.twig',[
// 'css' => $this->preference->getCookie(),
// 'pp' => "test2",
// 'user' => "Doe",
// 'role' => "Athlète",
// 'friendship' => [],
// 'analyzes' => [],
// 'mails' => [],
// 'users' => [],
// 'infoUser' => [],
// 'exos' => [],
// 'member' => []
// ]);
// }
#[Route(path: '/settings', name: 'settings', methods: ['GET'])]
public function settings(IRequest $req): Response
{
return $this->render('./page/settings.html.twig',[
'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
// #[Route(path: '/settings', name: 'settings', methods: ['GET'])]
// public function settings(IRequest $req): Response
// {
// return $this->render('./page/settings.html.twig',[
// 'css' => $this->preference->getCookie(),
// 'pp' => "test2",
// 'user' => "Doe",
// 'role' => "Athlète",
// 'friendship' => [],
// 'analyzes' => [],
// 'mails' => [],
// 'users' => [],
// 'infoUser' => [],
// 'exos' => [],
// 'member' => []
// ]);
// }
#[Route(path: '/preferences', name: 'preferences', methods: ['POST'])]
public function preferences(string $theme, IRequest $req): Response
{
// #[Route(path: '/preferences', name: 'preferences', methods: ['POST'])] // VALIDER LES DONNEES
// public function preferences(string $theme, IRequest $req): Response $this->preference->majCookie($theme);
// {
// /*TODO*/
// // VALIDER LES DONNEES return $this->render('./page/settings.html.twig',[
// $this->preference->majCookie($theme); 'css' => $this->preference->getCookie(),
'pp' => "test2",
'user' => "Doe",
'role' => "Athlète",
'friendship' => [],
'analyzes' => [],
'mails' => [],
'users' => [],
'infoUser' => [],
'exos' => [],
'member' => []
]);
}
// return $this->render('./page/settings.html.twig',[ }
// 'css' => $this->preference->getCookie(),
// 'pp' => "test2",
// 'user' => "Doe",
// 'role' => "Athlète",
// 'friendship' => [],
// 'analyzes' => [],
// 'mails' => [],
// 'users' => [],
// 'infoUser' => [],
// 'exos' => [],
// 'member' => []
// ]);
// }
// }

@ -0,0 +1,27 @@
<?php
namespace App\Router\Middleware;
use App\Router\Session;
use Shared\Log;
use App\Router\Request\IRequest;
use App\Router\Response\RedirectResponse;
class AuthMiddleware extends Middleware {
public function handle(IRequest $request, callable $next) {
// if (isset($_SESSION['user'])) {
// $resp =new RedirectResponse("/");
// $resp->send();
// exit;
// }
// La page nest pas redirigée correctement
// Firefox a détecté que le serveur redirige la demande pour cette adresse dune manière qui naboutira pas.
// La cause de ce problème peut être la désactivation ou le refus des cookies.
// if (!isset($_SESSION['user'])) {
// $resp =new RedirectResponse("/log");
// $resp->send();
// exit;
// }
return parent::handle($request, $next);
}
}

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>{% block title %}{% endblock %}</title>
<link href="/css/{% block css %}base_theme{% endblock %}.css" rel="stylesheet" />
<script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
</head>
<body class="bg-primary">
<div id="layoutAuthentication">
<div id="layoutAuthentication_content">
<main>
{% block main %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-5">
<div class="card-header"><h3 class="text-center font-weight-light my-4">Connexion</h3></div>
<div class="card-body">
<form>
<div class="form-floating mb-3">
<input class="form-control" id="inputEmail" type="email" placeholder="nom@exemple.com" />
<label for="inputEmail">Adresse eMail</label>
</div>
<div class="form-floating mb-3">
<input class="form-control" id="inputPassword" type="password" placeholder="Mot de passe" />
<label for="inputPassword">Mot de passe</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" id="inputRememberPassword" type="checkbox" value="" />
<label class="form-check-label" for="inputRememberPassword">Mémoriser le mot de passe</label>
</div>
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
<a class="small" href="password.html">Mot de passe oublié ?</a>
<a class="btn btn-primary" href="index.html">Se connecter</a>
</div>
</form>
</div>
<div class="card-footer text-center py-3">
<div class="small"><a href="register.html">Besoin d'un compte ? Inscrivez-vous !</a></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
</main>
</div>
<div id="layoutAuthentication_footer">
<footer class="py-4 bg-light mt-auto">
<div class="container-fluid px-4">
<div class="d-flex align-items-center justify-content-between small">
<div class="text-muted">Copyright &copy; HeartTrack 2023</div>
<div>
<a href="#">Politique de confidentialité</a>
&middot;
<a href="#">Termes &amp; Conditions d'utilisations</a>
</div>
</div>
</div>
</footer>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="js/scripts.js"></script>
</body>
</html>

@ -0,0 +1,40 @@
{% extends "authbase.html.twig" %}
{% block css %}{{css}}{% endblock %}
{% block title %}Connexion - HearthTrack{% endblock %}
{% block main %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-5">
<div class="card-header"><h3 class="text-center font-weight-light my-4">Connexion</h3></div>
<div class="card-body">
<form>
<div class="form-floating mb-3">
<input class="form-control" id="inputEmail" type="email" placeholder="nom@exemple.com" />
<label for="inputEmail">Adresse eMail</label>
</div>
<div class="form-floating mb-3">
<input class="form-control" id="inputPassword" type="password" placeholder="Mot de passe" />
<label for="inputPassword">Mot de passe</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" id="inputRememberPassword" type="checkbox" value="" />
<label class="form-check-label" for="inputRememberPassword">Mémoriser le mot de passe</label>
</div>
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
<a class="small" href="password.html">Mot de passe oublié ?</a>
<a class="btn btn-primary" href="index.html">Se connecter</a>
</div>
</form>
</div>
<div class="card-footer text-center py-3">
<div class="small"><a href="register.html">Besoin d'un compte ? Inscrivez-vous !</a></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

@ -0,0 +1,95 @@
{% extends "authbase.html.twig" %}
{% block css %}{{css}}{% endblock %}
{% block title %}Inscription - HearthTrack{% endblock %}
{% block main %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-7">
<div class="card shadow-lg border-0 rounded-lg mt-5">
<div class="card-header"><h3 class="text-center font-weight-light my-4">Créer un compte</h3></div>
<div class="card-body">
<form>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-floating mb-3 mb-md-0">
<input class="form-control" id="inputFirstName" type="text" placeholder="Entrez votre nom" />
<label for="inputFirstName">Nom de famille</label>
</div>
</div>
<div class="col-md-6">
<div class="form-floating">
<input class="form-control" id="inputLastName" type="text" placeholder="Entrez votre prénom" />
<label for="inputLastName">Prénom</label>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-floating mb-3 mb-md-0">
<input class="form-control" id="inputUsername" type="text" placeholder="Entrez votre pseudonyme" />
<label for="inputFirstName">Nom d'utilisateur</label>
</div>
</div>
<div class="col-md-6">
<div class="form-floating">
<label for="inputLastName">Sexe</label>
<select id="gender" name="gender">
<option value="male">Homme</option>
<option value="female">Femme</option>
<option value="unknown">Ne se prononce pas</option>
</select>
</div>
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" id="inputEmail" type="email" placeholder="nom@exemple.com" />
<label for="inputEmail">Adresse eMail</label>
</div>
<div class="form-floating mb-3">
<input class="form-control" id="inputDateNaissance" type="date" placeholder="" />
<label for="inputEmail">Date de naissance</label>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-floating mb-3 mb-md-0">
<input class="form-control" id="inputTaille" type="text" placeholder="Entrez votre taille" />
<label for="inputPassword">Taille</label>
</div>
</div>
<div class="col-md-6">
<div class="form-floating mb-3 mb-md-0">
<input class="form-control" id="inputPoids" type="text" placeholder="Entrez votre poids" />
<label for="inputPasswordConfirm">Poids</label>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-floating mb-3 mb-md-0">
<input class="form-control" id="inputPassword" type="password" placeholder="Créez un mot de passe" />
<label for="inputPassword">Mot de passe</label>
</div>
</div>
<div class="col-md-6">
<div class="form-floating mb-3 mb-md-0">
<input class="form-control" id="inputPasswordConfirm" type="password" placeholder="Confirmez votre mot de passe" />
<label for="inputPasswordConfirm">Confirmer le mot de passe</label>
</div>
</div>
</div>
<div class="mt-4 mb-0">
<div class="d-grid"><a class="btn btn-primary btn-block" href="login.html.twig">Créer un compte</a></div>
</div>
</form>
</div>
<div class="card-footer text-center py-3">
<div class="small"><a href="login.html.twig">Avez-vous déjà un compte ? Connectez-vous ?</a></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

@ -0,0 +1,86 @@
<?php
namespace Data\Core;
use Model\User;
use Model\Athlete;
use Model\CoachAthlete;
use Repository\IUserRepository;
use Shared\IHashPassword;
class AuthService implements IAuthService {
private IUserRepository $userRepository;
private IHashPassword $passwordHacher;
private ?User $currentUser = null;
public function __construct(IUserRepository $userRepository, IHashPassword $passwordHacher) {
$this->userRepository = $userRepository;
$this->passwordHacher = $passwordHacher;
}
public function login(string $email, string $password): bool {
$user = $this->userRepository->getItemByEmail($email);
if ($user === null || !$this->validatePassword($password, $user->getPasswordHash())) {
return false;
}
$this->currentUser = $user;
// Add session handling logic here
return true;
}
public function register(string $email, string $password, array $data): bool {
if ($this->userRepository->getItemByEmail($email)) {
throw new \Exception('User already exists');
}
$hashedPassword = $this->passwordHacher->hashPassword($password);
$prenom = $data['prenom'];
$username = $data['username'];
$nom = $data['nom'];
$email = $data['email'];
$sexe = $data['sexe'];
$taille = $data['taille'];
$poids = $data['poids'];
$dateNaissance = $data['dateNaissance'] ;
$roleName = $data['roleName'];
$role = null;
if($roleName == "Coach"){
$role = new CoachAthlete();
}
else if($roleName == "Athlete"){
$role = new Athlete();
}
// Create a new user instance (you should expand on this with more data as needed)
$user = new User(
random_int(0, 100),
$nom,
$prenom,
$username,
$email,
$hashedPassword,
$sexe,
$taille,
$poids,
$dateNaissance,
//should use reflexion
$role
);
$this->userRepository->addItem($user);
$this->currentUser = $user;
// Add session handling logic here
return true;
}
public function logout(): void {
$this->currentUser = null;
// Add session handling logic here
}
public function getCurrentUser(): ?User {
return $this->currentUser;
}
private function validatePassword(string $password, string $hash): bool {
// Implement password validation logic (e.g., using password_verify if using bcrypt)
}
}

@ -25,8 +25,8 @@
$this->cookie = $maj; $this->cookie = $maj;
setcookie('preferences', $maj); setcookie('preferences', $maj);
} }
} catch (Exception $e){ } catch (\Exception $e){
throw new ValueError; throw new \ValueError;
} }
} }

@ -13,24 +13,16 @@ use Stub\UserRepository;
class UserManager class UserManager
{ {
/**
* @return User
*/
public function getCurrentUser(): User
{
return $this->currentUser;
}
private IAuthService $authService; private IAuthService $authService;
// private IFriendRequestService $friendService; private ?User $currentUser;
private User $currentUser;
private DataManager $dataManager; private DataManager $dataManager;
private IFriendRequestService $relationshipService; private IFriendRequestService $relationshipService;
public function __construct(DataManager $dataManager, IAuthService $authService, IFriendRequestService $relationshipService) public function __construct(DataManager $dataManager, IAuthService $authService, IFriendRequestService $relationshipService)
{ {
$this->authService = $authService; $this->authService = $authService;
@ -38,6 +30,10 @@ class UserManager
$this->relationshipService = $relationshipService; $this->relationshipService = $relationshipService;
} }
public function getCurrentUser(): ?User
{
return $this->currentUser;
}
public function login($emailUser, $passwordUser): bool public function login($emailUser, $passwordUser): bool
{ {

@ -0,0 +1,11 @@
engines:
phpmd:
enabled: true
ratings:
paths:
- "**.php"
exclude_paths:
- "demo/"
- "tests/"

@ -0,0 +1,17 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

@ -0,0 +1,49 @@
vendor
issues
composer.phar
composer.lock
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
# =========================
# Operating System Files
# =========================
# OSX
# =========================
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
/nbproject

@ -0,0 +1,15 @@
dist: trusty
language: php
php:
- 5.5
before_script:
- curl -s http://getcomposer.org/installer | php
- php composer.phar install --dev --no-interaction
script:
- mkdir -p build/logs
- phpunit --coverage-clover build/logs/clover.xml tests
after_script:
- php vendor/bin/coveralls -v

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Adrian Gibbons
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,383 @@
[![Build Status](https://travis-ci.org/adriangibbons/php-fit-file-analysis.svg?branch=master)](https://travis-ci.org/adriangibbons/php-fit-file-analysis) [![Packagist](https://img.shields.io/packagist/v/adriangibbons/php-fit-file-analysis.svg)](https://packagist.org/packages/adriangibbons/php-fit-file-analysis) [![Packagist](https://img.shields.io/packagist/dt/adriangibbons/php-fit-file-analysis.svg)](https://packagist.org/packages/adriangibbons/php-fit-file-analysis) [![Coverage Status](https://coveralls.io/repos/adriangibbons/php-fit-file-analysis/badge.svg?branch=master&service=github)](https://coveralls.io/github/adriangibbons/php-fit-file-analysis?branch=master)
# phpFITFileAnalysis
A PHP (>= v5.4) class for analysing FIT files created by Garmin GPS devices.
[Live demonstration](http://adriangibbons.com/php-fit-file-analysis/demo/) (Right-click and Open in new tab)
## Demo Screenshots
![Mountain Biking](demo/img/mountain-biking.jpg)
![Power Analysis](demo/img/power-analysis.jpg)
![Quadrant Analysis](demo/img/quadrant-analysis.jpg)
![Swim](demo/img/swim.jpg)
Please read this page in its entirety and the [FAQ](https://github.com/adriangibbons/php-fit-file-analysis/wiki/Frequently-Asked-Questions-(FAQ)) first if you have any questions or need support.
## What is a FIT file?
FIT or Flexible and Interoperable Data Transfer is a file format used for GPS tracks and routes. It is used by newer Garmin fitness GPS devices, including the Edge and Forerunner series, which are popular with cyclists and runners.
Visit the FAQ page within the Wiki for more information.
## How do I use phpFITFileAnalysis with my PHP-driven website?
A couple of choices here:
**The more modern way:** Add the package *adriangibbons/php-fit-file-analysis* in a composer.json file:
```JSON
{
"require": {
"adriangibbons/php-fit-file-analysis": "^3.2.0"
}
}
```
Run ```composer update``` from the command line.
The composer.json file should autoload the ```phpFITFileAnalysis``` class, so as long as you include the autoload file in your PHP file, you should be able to instantiate the class with:
```php
<?php
require __DIR__ . '/vendor/autoload.php'; // this file is in the project's root folder
$pFFA = new adriangibbons\phpFITFileAnalysis('fit_files/my_fit_file.fit');
?>
```
**The more manual way:** Download the ZIP from GitHub and put PHP class file from the /src directory somewhere appropriate (e.g. classes/). A conscious effort has been made to keep everything in a single file.
Then include the file on the PHP page where you want to use it and instantiate an object of the class:
```php
<?php
include('classes/phpFITFileAnalysis.php');
$pFFA = new adriangibbons\phpFITFileAnalysis('fit_files/my_fit_file.fit');
?>
```
Note that the only mandatory parameter required when creating an instance is the path to the FIT file that you want to load.
There are more **Optional Parameters** that can be supplied. These are described in more detail further down this page.
The object will automatically load the FIT file and iterate through its contents. It will store any data it finds in arrays, which are accessible via the public data variable.
### Accessing the Data
Data read by the class are stored in associative arrays, which are accessible via the public data variable:
```php
$pFFA->data_mesgs
```
The array indexes are the names of the messages and fields that they contain. For example:
```php
// Contains an array of all heart_rate data read from the file, indexed by timestamp
$pFFA->data_mesgs['record']['heart_rate']
// Contains an integer identifying the number of laps
$pFFA->data_mesgs['session']['num_laps']
```
**OK, but how do I know what messages and fields are in my file?**
You could either iterate through the $pFFA->data_mesgs array, or take a look at the debug information you can dump to a webpage:
```php
// Option 1. Iterate through the $pFFA->data_mesgs array
foreach ($pFFA->data_mesgs as $mesg_key => $mesg) { // Iterate the array and output the messages
echo "<strong>Found Message: $mesg_key</strong><br>";
foreach ($mesg as $field_key => $field) { // Iterate each message and output the fields
echo " - Found Field: $mesg_key -> $field_key<br>";
}
echo "<br>";
}
// Option 2. Show the debug information
$pFFA->showDebugInfo(); // Quite a lot of info...
```
**How about some real-world examples?**
```php
// Get Max and Avg Speed
echo "Maximum Speed: ".max($pFFA->data_mesgs['record']['speed'])."<br>";
echo "Average Speed: ".( array_sum($pFFA->data_mesgs['record']['speed']) / count($pFFA->data_mesgs['record']['speed']) )."<br>";
// Put HR data into a JavaScript array for use in a Chart
echo "var chartData = [";
foreach ($pFFA->data_mesgs['record']['heart_rate'] as $timestamp => $hr_value) {
echo "[$timestamp,$hr_value],";
}
echo "];";
```
**Enumerated Data**
The FIT protocol makes use of enumerated data types. Where these values have been identified in the FIT SDK, they have been included in the class as a private variable: $enum_data.
A public function is available, which will return the enumerated value for a given message type. For example:
```php
// Access data stored within the private class variable $enum_data
// $pFFA->enumData($type, $value)
// e.g.
echo $pFFA->enumData('sport', 2)); // returns 'cycling'
echo $pFFA->enumData('manufacturer', $this->data_mesgs['device_info']['manufacturer']); // returns 'Garmin';
echo $pFFA->manufacturer(); // Short-hand for above
```
In addition, public functions provide a short-hand way to access commonly used enumerated data:
- manufacturer()
- product()
- sport()
### Optional Parameters
There are five optional parameters that can be passed as an associative array when the phpFITFileAnalysis object is instantiated. These are:
- fix_data
- data_every_second
- units
- pace
- garmin_timestamps
- overwrite_with_dev_data
For example:
```php
$options = [
'fix_data' => ['cadence', 'distance'],
'data_every_second' => true
'units' => 'statute',
'pace' => true,
'garmin_timestamps' => true,
'overwrite_with_dev_data' => false
];
$pFFA = new adriangibbons\phpFITFileAnalysis('my_fit_file.fit', $options);
```
The optional parameters are described in more detail below.
#### "Fix" the Data
FIT files have been observed where some data points are missing for one sensor (e.g. cadence/foot pod), where information has been collected for other sensors (e.g. heart rate) at the same instant. The cause is unknown and typically only a relatively small number of data points are missing. Fixing the issue is probably unnecessary, as each datum is indexed using a timestamp. However, it may be important for your project to have the exact same number of data points for each type of data.
**Recognised values:** 'all', 'cadence', 'distance', 'heart_rate', 'lat_lon', 'power', 'speed'
**Examples: **
```php
$options = ['fix_data' => ['all']]; // fix cadence, distance, heart_rate, lat_lon, power, and speed data
$options = ['fix_data' => ['cadence', 'distance']]; // fix cadence and distance data only
$options = ['fix_data' => ['lat_lon']]; // fix position data only
```
If the *fix_data* array is not supplied, then no "fixing" of the data is performed.
A FIT file might contain the following:
<table>
<thead>
<th></th>
<th># Data Points</th>
<th>Delta (c.f. Timestamps)</th>
</thead>
<tbody>
<tr>
<td>timestamp</td><td>10251</td><td>0</td>
</tr>
<tr>
<td>position_lat</td><td>10236</td><td>25</td>
</tr>
<tr>
<td>position_long</td><td>10236</td><td>25</td>
</tr>
<tr>
<td>altitude</td><td>10251</td><td>0</td>
</tr>
<tr>
<td>heart_rate</td><td>10251</td><td>0</td>
</tr>
<tr>
<td>cadence</td><td>9716</td><td>535</td>
</tr>
<tr>
<td>distance</td><td>10236</td><td>25</td>
</tr>
<tr>
<td>speed</td><td>10236</td><td>25</td>
</tr>
<tr>
<td>power</td><td>10242</td><td>9</td>
</tr>
<tr>
<td>temperature</td><td>10251</td><td>0</td>
</tr>
</tbody>
</table>
As illustrated above, the types of data most susceptible to missing data points are: position_lat, position_long, altitude, heart_rate, cadence, distance, speed, and power.
With the exception of cadence information, missing data points are "fixed" by inserting interpolated values.
For cadence, zeroes are inserted as it is thought that it is likely no data has been collected due to a lack of movement at that point in time.
**Interpolation of missing data points**
```php
// Do not use code, just for demonstration purposes
var_dump($pFFA->data_mesgs['record']['temperature']); // ['100'=>22, '101'=>22, '102'=>23, '103'=>23, '104'=>23];
var_dump($pFFA->data_mesgs['record']['distance']); // ['100'=>3.62, '101'=>4.01, '104'=>10.88];
```
As you can see from the trivial example above, temperature data have been recorded for each of five timestamps (100, 101, 102, 103, and 104). However, distance information has not been recorded for timestamps 102 and 103.
If *fix_data* includes 'distance', then the class will attempt to insert data into the distance array with the indexes 102 and 103. Values are determined using a linear interpolation between indexes 101(4.01) and 104(10.88).
The result would be:
```php
var_dump($pFFA->data_mesgs['record']['distance']); // ['100'=>3.62, '101'=>4.01, '102'=>6.30, '103'=>8.59, '104'=>10.88];
```
#### Data Every Second
Some of Garmin's Fitness devices offer the choice of Smart Recording or Every Second Recording.
Smart Recording records key points where the fitness device changes direction, speed, heart rate or elevation. This recording type records less track points and will potentially have gaps between timestamps of greater than one second.
You can force timestamps to be regular one second intervals by setting the option:
```php
$options = ['data_every_second' => true];
```
Missing timestamps will have data interpolated as per the ```fix_data``` option above.
If the ```fix_data``` option is not specified in conjunction with ```data_every_second``` then ```'fix_data' => ['all']``` is assumed.
*Note that you may experience degraded performance using the ```fix_data``` option. Improving the performance will be explored - it is likely the ```interpolateMissingData()``` function is sub-optimal.*
#### Set Units
By default, **metric** units (identified in the table below) are assumed.
<table>
<thead>
<th></th>
<th>Metric<br><em>(DEFAULT)</em></th>
<th>Statute</th>
<th>Raw</th>
</thead>
<tbody>
<tr>
<td>Speed</td><td>kilometers per hour</td><td>miles per hour</td><td>meters per second</td>
</tr>
<tr>
<td>Distance</td><td>kilometers</td><td>miles</td><td>meters</td>
</tr>
<tr>
<td>Altitude</td><td>meters</td><td>feet</td><td>meters</td>
</tr>
<tr>
<td>Latitude</td><td>degrees</td><td>degrees</td><td>semicircles</td>
</tr>
<tr>
<td>Longitude</td><td>degrees</td><td>degrees</td><td>semicircles</td>
</tr>
<tr>
<td>Temperature</td><td>celsius (&#8451;)</td><td>fahrenheit (&#8457;)</td><td>celsius (&#8451;)</td>
</tr>
</tbody>
</table>
You can request **statute** or **raw** units instead of metric. Raw units are those were used by the device that created the FIT file and are native to the FIT standard (i.e. no transformation of values read from the file will occur).
To select the units you require, use one of the following:
```php
$options = ['units' => 'statute'];
$options = ['units' => 'raw'];
$options = ['units' => 'metric']; // explicit but not necessary, same as default
```
#### Pace
If required by the user, pace can be provided instead of speed. Depending on the units requested, pace will either be in minutes per kilometre (min/km) for metric units; or minutes per mile (min/mi) for statute.
To select pace, use the following option:
```php
$options = ['pace' => true];
```
Pace values will be decimal minutes. To get the seconds, you may wish to do something like:
```php
foreach ($pFFA->data_mesgs['record']['speed'] as $key => $value) {
$min = floor($value);
$sec = round(60 * ($value - $min));
echo "pace: $min min $sec sec<br>";
}
```
Note that if 'raw' units are requested then this parameter has no effect on the speed data, as it is left untouched from what was read-in from the file.
#### Timestamps
Unix time is the number of seconds since **UTC 00:00:00 Jan 01 1970**, however the FIT standard specifies that timestamps (i.e. fields of type date_time and local_date_time) represent seconds since **UTC 00:00:00 Dec 31 1989**.
The difference (in seconds) between FIT and Unix timestamps is 631,065,600:
```php
$date_FIT = new DateTime('1989-12-31 00:00:00', new DateTimeZone('UTC'));
$date_UNIX = new DateTime('1970-01-01 00:00:00', new DateTimeZone('UTC'));
$diff = $date_FIT->getTimestamp() - $date_UNIX->getTimestamp();
echo 'The difference (in seconds) between FIT and Unix timestamps is '. number_format($diff);
```
By default, fields of type date_time and local_date_time read from FIT files will have this delta added to them so that they can be treated as Unix time. If the FIT timestamp is required, the 'garmin_timestamps' option can be set to true.
#### Overwrite with Developer Data
The FIT standard allows developers to define the meaning of data without requiring changes to the FIT profile being used. They may define data that is already incorporated in the standard - e.g. HR, cadence, power, etc. By default, if developers do this, the data will overwrite anything in the regular ```$pFFA->data_mesgs['record']``` array. If you do not want this occur, set the 'overwrite_with_dev_data' option to false. The data will still be available in ```$pFFA->data_mesgs['developer_data']```.
## Analysis
The following functions return arrays of data that could be used to create tables/charts:
```php
array $pFFA->hrPartionedHRmaximum(int $hr_maximum);
array $pFFA->hrPartionedHRreserve(int $hr_resting, int $hr_maximum);
array $pFFA->powerPartioned(int $functional_threshold_power);
array $pFFA->powerHistogram(int $bucket_width = 25);
```
For advanced control over these functions, or use with other sensor data (e.g. cadence or speed), use the underlying functions:
```php
array $pFFA->partitionData(string $record_field='', $thresholds=null, bool $percentages = true, bool $labels_for_keys = true);
array $pFFA->histogram(int $bucket_width=25, string $record_field='');
```
Functions exist to determine thresholds based on percentages of user-supplied data:
```php
array $pFFA->hrZonesMax(int $hr_maximum, array $percentages_array=[0.60, 0.75, 0.85, 0.95]);
array $pFFA->hrZonesReserve(int $hr_resting, int $hr_maximum, array $percentages_array=[0.60, 0.65, 0.75, 0.82, 0.89, 0.94 ]) {
array $pFFA->powerZones(int $functional_threshold_power, array $percentages_array=[0.55, 0.75, 0.90, 1.05, 1.20, 1.50]);
```
### Heart Rate
A function exists for analysing heart rate data:
```php
// hr_FT is heart rate at Functional Threshold, or Lactate Threshold Heart Rate
array $pFFA->hrMetrics(int $hr_resting, int $hr_maximum, string $hr_FT, $gender);
// e.g. $pFFA->hrMetrics(52, 189, 172, 'male');
```
**Heart Rate metrics:**
* TRIMP (TRaining IMPulse)
* Intensity Factor
### Power
Three functions exist for analysing power data:
```php
array $pFFA->powerMetrics(int $functional_threshold_power);
array $pFFA->criticalPower(int or array $time_periods); // e.g. 300 or [600, 900]
array $pFFA->quadrantAnalysis(float $crank_length, int $ftp, int $selected_cadence = 90, bool $use_timestamps = false); // Crank length in metres
```
**Power metrics:**
* Average Power
* Kilojoules
* Normalised Power (estimate had your power output been constant)
* Variability Index (ratio of Normalised Power / Average Power)
* Intensity Factor (ratio of Normalised Power / Functional Threshold Power)
* Training Stress Score (effort based on relative intensity and duration)
**Critical Power** (or Best Effort) is the highest average power sustained for a specified period of time within the activity. You can supply a single time period (in seconds), or an array or time periods.
**Quadrant Analysis** provides insight into the neuromuscular demands of a bike ride through comparing pedal velocity with force by looking at cadence and power.
Note that ```$pFFA->criticalPower``` and some power metrics (Normalised Power, Variability Index, Intensity Factor, Training Stress Score) will use the [PHP Trader](http://php.net/manual/en/book.trader.php) extension if it is loaded on the server. If the extension is not loaded then it will use the built-in Simple Moving Average algorithm, which is far less performant particularly for larger files!
A demo of power analysis is available [here](http://adriangibbons.com/php-fit-file-analysis/demo/power-analysis.php).
## Other methods
Returns array of booleans using timestamp as key. true == timer paused (e.g. autopause):
```php
array isPaused()
```
Returns a JSON object with requested ride data:
```php
array getJSON(float $crank_length = null, int $ftp = null, array $data_required = ['all'], int $selected_cadence = 90)
/**
* $data_required can be ['all'] or a combination of:
* ['timestamp', 'paused', 'temperature', 'lap', 'position_lat', 'position_long', 'distance', 'altitude', 'speed', 'heart_rate', 'cadence', 'power', 'quadrant-analysis']
*/
```
Returns array of gear change information (if present, e.g. using Shimano D-Fly Wireless Di2 Transmitter):
```php
// By default, time spent in a gear whilst the timer is paused (e.g. autopause) is ignored. Set to false to include.
array gearChanges($bIgnoreTimerPaused = true)
```
## Acknowledgement
This class has been created using information available in a Software Development Kit (SDK) made available by ANT ([thisisant.com](http://www.thisisant.com/resources/fit)).
As a minimum, I'd recommend reading the three PDFs included in the SDK:
1. FIT File Types Description
2. FIT SDK Introductory Guide
3. Flexible & Interoperable Data Transfer (FIT) Protocol
Following these, the 'Profile.xls' spreadsheet and then the Java/C/C++ examples.

@ -0,0 +1,17 @@
{
"name": "adriangibbons/php-fit-file-analysis",
"type": "library",
"description": "A PHP class for analysing FIT files created by Garmin GPS devices",
"keywords": ["garmin", "fit"],
"homepage": "https://github.com/adriangibbons/php-fit-file-analysis",
"require-dev": {
"phpunit/phpunit": "4.8.*",
"squizlabs/php_codesniffer": "2.*",
"satooshi/php-coveralls": "^2.0"
},
"autoload": {
"psr-4": {
"adriangibbons\\": "src/"
}
}
}

@ -0,0 +1,319 @@
#lap-row-chart svg g g.axis { display: none; }
div.dc-chart {
float: left;
}
.dc-chart rect.bar {
stroke: none;
cursor: pointer;
}
.dc-chart rect.bar:hover {
fill-opacity: .5;
}
.dc-chart rect.stack1 {
stroke: none;
fill: red;
}
.dc-chart rect.stack2 {
stroke: none;
fill: green;
}
.dc-chart rect.deselected {
stroke: none;
fill: #ccc;
}
.dc-chart .empty-chart .pie-slice path {
fill: #FFEEEE;
cursor: default;
}
.dc-chart .empty-chart .pie-slice {
cursor: default;
}
.dc-chart .pie-slice {
fill: white;
font-size: 12px;
cursor: pointer;
}
.dc-chart .pie-slice.external{
fill: black;
}
.dc-chart .pie-slice :hover {
fill-opacity: .8;
}
.dc-chart .pie-slice.highlight {
fill-opacity: .8;
}
.dc-chart .selected path {
stroke-width: 3;
stroke: #ccc;
fill-opacity: 1;
}
.dc-chart .deselected path {
stroke: none;
fill-opacity: .5;
fill: #ccc;
}
.dc-chart .axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dc-chart .axis text {
font: 10px sans-serif;
}
.dc-chart .grid-line {
fill: none;
stroke: #ccc;
opacity: .5;
shape-rendering: crispEdges;
}
.dc-chart .grid-line line {
fill: none;
stroke: #ccc;
opacity: .5;
shape-rendering: crispEdges;
}
.dc-chart .brush rect.background {
z-index: -999;
}
.dc-chart .brush rect.extent {
fill: steelblue;
fill-opacity: .125;
}
.dc-chart .brush .resize path {
fill: #eee;
stroke: #666;
}
.dc-chart path.line {
fill: none;
stroke-width: 1.5px;
}
.dc-chart circle.dot {
stroke: none;
}
.dc-chart g.dc-tooltip path {
fill: none;
stroke: grey;
stroke-opacity: .8;
}
.dc-chart path.area {
fill-opacity: .3;
stroke: none;
}
.dc-chart .node {
font-size: 0.7em;
cursor: pointer;
}
.dc-chart .node :hover {
fill-opacity: .8;
}
.dc-chart .selected circle {
stroke-width: 3;
stroke: #ccc;
fill-opacity: 1;
}
.dc-chart .deselected circle {
stroke: none;
fill-opacity: .5;
fill: #ccc;
}
.dc-chart .bubble {
stroke: none;
fill-opacity: 0.6;
}
.dc-data-count {
float: right;
margin-top: 15px;
margin-right: 15px;
}
.dc-data-count .filter-count {
color: #3182bd;
font-weight: bold;
}
.dc-data-count .total-count {
color: #3182bd;
font-weight: bold;
}
.dc-data-table {
}
.dc-chart g.state {
cursor: pointer;
}
.dc-chart g.state :hover {
fill-opacity: .8;
}
.dc-chart g.state path {
stroke: white;
}
.dc-chart g.selected path {
}
.dc-chart g.deselected path {
fill: grey;
}
.dc-chart g.selected text {
}
.dc-chart g.deselected text {
display: none;
}
.dc-chart g.county path {
stroke: white;
fill: none;
}
.dc-chart g.debug rect {
fill: blue;
fill-opacity: .2;
}
.dc-chart g.row rect {
fill-opacity: 0.8;
cursor: pointer;
}
.dc-chart g.row rect:hover {
fill-opacity: 0.6;
}
.dc-chart g.row text {
fill: #333;
font-size: 12px;
cursor: pointer;
}
.dc-legend {
font-size: 11px;
}
.dc-legend-item {
cursor: pointer;
}
.dc-chart g.axis text {
/* Makes it so the user can't accidentally click and select text that is meant as a label only */
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10 */
-o-user-select: none;
user-select: none;
pointer-events: none;
}
.dc-chart path.highlight {
stroke-width: 3;
fill-opacity: 1;
stroke-opacity: 1;
}
.dc-chart .highlight {
fill-opacity: 1;
stroke-opacity: 1;
}
.dc-chart .fadeout {
fill-opacity: 0.2;
stroke-opacity: 0.2;
}
.dc-chart path.dc-symbol, g.dc-legend-item.fadeout {
fill-opacity: 0.5;
stroke-opacity: 0.5;
}
.dc-hard .number-display {
float: none;
}
.dc-chart .box text {
font: 10px sans-serif;
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10 */
-o-user-select: none;
user-select: none;
pointer-events: none;
}
.dc-chart .box line,
.dc-chart .box circle {
fill: #fff;
stroke: #000;
stroke-width: 1.5px;
}
.dc-chart .box rect {
stroke: #000;
stroke-width: 1.5px;
}
.dc-chart .box .center {
stroke-dasharray: 3,3;
}
.dc-chart .box .outlier {
fill: none;
stroke: #ccc;
}
.dc-chart .box.deselected .box {
fill: #ccc;
}
.dc-chart .box.deselected {
opacity: .5;
}
.dc-chart .symbol{
stroke: none;
}
.dc-chart .heatmap .box-group.deselected rect {
stroke: none;
fill-opacity: .5;
fill: #ccc;
}
.dc-chart .heatmap g.axis text {
pointer-events: all;
cursor: pointer;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@ -0,0 +1,61 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>phpFITFileAnalysis demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div class="jumbotron">
<div class="container">
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
</div>
</div>
<div class="container">
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> Mountain Biking</h3>
</div>
<div class="panel-body text-center">
<a href="mountain-biking.php"><img src="img/mountain-biking.jpg" alt="Mountain Biking"></a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> Power Analysis <small>(cycling)</small></h3>
</div>
<div class="panel-body text-center">
<a href="power-analysis.php"><img src="img/power-analysis.jpg" alt="Power Analysis"></a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> Quadrant Analysis</h3>
</div>
<div class="panel-body text-center">
<a href="quadrant-analysis.php"><img src="img/quadrant-analysis.jpg" alt="Quadrant Analysis"></a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> Swim</h3>
</div>
<div class="panel-body text-center">
<a href="swim.php"><img src="img/swim.jpg" alt="Swim"></a>
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,83 @@
<?php
/*
* A Ramer-Douglas-Peucker implementation to simplify lines in PHP
* Unlike the one by Pol Dell'Aiera this one is built to operate on an array of arrays and in a non-OO manner,
* making it suitable for smaller apps which must consume input from ArcGIS or Leaflet, without the luxury of GeoPHP/GEOS
*
* Usage:
* $verts = array( array(0,1), array(1,2), array(2,1), array(3,5), array(4,6), array(5,5) );
* $tolerance = 1.0;
* $newverts = simplify_RDP($verts,$tolerance);
*
* Bonus: It does not trim off extra ordinates from each vertex, so it's agnostic as to whether your data are 2D or 3D
* and will return the kept vertices unchanged.
*
* This operates on a single set of vertices, aka a single linestring.
* If used on a multilinestring you will want to run it on each component linestring separately.
*
* No license, use as you will, credits appreciated but not required, etc.
* Greg Allensworth, GreenInfo Network <gregor@greeninfo.org>
*
* My invaluable references:
* https://github.com/Polzme/geoPHP/commit/56c9072f69ed1cec2fdd36da76fa595792b4aa24
* http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
* http://math.ucsd.edu/~wgarner/math4c/derivations/distance/distptline.htm
*/
function simplify_RDP($vertices, $tolerance) {
// if this is a multilinestring, then we call ourselves one each segment individually, collect the list, and return that list of simplified lists
if (is_array($vertices[0][0])) {
$multi = array();
foreach ($vertices as $subvertices) $multi[] = simplify_RDP($subvertices,$tolerance);
return $multi;
}
$tolerance2 = $tolerance * $tolerance;
// okay, so this is a single linestring and we simplify it individually
return _segment_RDP($vertices,$tolerance2);
}
function _segment_RDP($segment, $tolerance_squared) {
if (sizeof($segment) <= 2) return $segment; // segment is too small to simplify, hand it back as-is
// find the maximum distance (squared) between this line $segment and each vertex
// distance is solved as described at UCSD page linked above
// cheat: vertical lines (directly north-south) have no slope so we fudge it with a very tiny nudge to one vertex; can't imagine any units where this will matter
$startx = (float) $segment[0][0];
$starty = (float) $segment[0][1];
$endx = (float) $segment[ sizeof($segment)-1 ][0];
$endy = (float) $segment[ sizeof($segment)-1 ][1];
if ($endx == $startx) $startx += 0.00001;
$m = ($endy - $starty) / ($endx - $startx); // slope, as in y = mx + b
$b = $starty - ($m * $startx); // y-intercept, as in y = mx + b
$max_distance_squared = 0;
$max_distance_index = null;
for ($i=1, $l=sizeof($segment); $i<=$l-2; $i++) {
$x1 = $segment[$i][0];
$y1 = $segment[$i][1];
$closestx = ( ($m*$y1) + ($x1) - ($m*$b) ) / ( ($m*$m)+1);
$closesty = ($m * $closestx) + $b;
$distsqr = ($closestx-$x1)*($closestx-$x1) + ($closesty-$y1)*($closesty-$y1);
if ($distsqr > $max_distance_squared) {
$max_distance_squared = $distsqr;
$max_distance_index = $i;
}
}
// cleanup and disposition
// if the max distance is below tolerance, we can bail, giving a straight line between the start vertex and end vertex (all points are so close to the straight line)
if ($max_distance_squared <= $tolerance_squared) {
return array($segment[0], $segment[ sizeof($segment)-1 ]);
}
// but if we got here then a vertex falls outside the tolerance
// split the line segment into two smaller segments at that "maximum error vertex" and simplify those
$slice1 = array_slice($segment, 0, $max_distance_index);
$slice2 = array_slice($segment, $max_distance_index);
$segs1 = _segment_RDP($slice1, $tolerance_squared);
$segs2 = _segment_RDP($slice2, $tolerance_squared);
return array_merge($segs1,$segs2);
}

@ -0,0 +1,104 @@
<?php
/*!
* PHP Polyline Encoder
*
*/
class PolylineEncoder {
private $points;
private $encoded;
public function __construct() {
$this->points = array();
}
/**
* Add a point
*
* @param float $lat : lattitude
* @param float $lng : longitude
*/
function addPoint($lat, $lng) {
if (empty($this->points)) {
$this->points[] = array('x' => $lat, 'y' => $lng);
$this->encoded = $this->encodeValue($lat) . $this->encodeValue($lng);
} else {
$n = count($this->points);
$prev_p = $this->points[$n-1];
$this->points[] = array('x' => $lat, 'y' => $lng);
$this->encoded .= $this->encodeValue($lat-$prev_p['x']) . $this->encodeValue($lng-$prev_p['y']);
}
}
/**
* Return the encoded string generated from the points
*
* @return string
*/
function encodedString() {
return $this->encoded;
}
/**
* Encode a value following Google Maps API v3 algorithm
*
* @param type $value
* @return type
*/
function encodeValue($value) {
$encoded = "";
$value = round($value * 100000);
$r = ($value < 0) ? ~($value << 1) : ($value << 1);
while ($r >= 0x20) {
$val = (0x20|($r & 0x1f)) + 63;
$encoded .= chr($val);
$r >>= 5;
}
$lastVal = $r + 63;
$encoded .= chr($lastVal);
return $encoded;
}
/**
* Decode an encoded polyline string to an array of points
*
* @param type $value
* @return type
*/
static public function decodeValue($value) {
$index = 0;
$points = array();
$lat = 0;
$lng = 0;
while ($index < strlen($value)) {
$b;
$shift = 0;
$result = 0;
do {
$b = ord(substr($value, $index++, 1)) - 63;
$result |= ($b & 0x1f) << $shift;
$shift += 5;
} while ($b > 31);
$dlat = (($result & 1) ? ~($result >> 1) : ($result >> 1));
$lat += $dlat;
$shift = 0;
$result = 0;
do {
$b = ord(substr($value, $index++, 1)) - 63;
$result |= ($b & 0x1f) << $shift;
$shift += 5;
} while ($b > 31);
$dlng = (($result & 1) ? ~($result >> 1) : ($result >> 1));
$lng += $dlng;
$points[] = array('x' => $lat/100000, 'y' => $lng/100000);
}
return $points;
}
}

@ -0,0 +1,298 @@
<?php
/**
* Demonstration of the phpFITFileAnalysis class using Twitter Bootstrap framework
* https://github.com/adriangibbons/phpFITFileAnalysis
*
* Not intended to be demonstration of how to best use Google APIs, but works for me!
*
* If you find this useful, feel free to drop me a line at Adrian.GitHub@gmail.com
*/
require __DIR__ . '/../src/phpFITFileAnalysis.php';
require __DIR__ . '/libraries/PolylineEncoder.php'; // https://github.com/dyaaj/polyline-encoder
require __DIR__ . '/libraries/Line_DouglasPeucker.php'; // https://github.com/gregallensworth/PHP-Geometry
try {
$file = '/fit_files/mountain-biking.fit';
$options = [
// Just using the defaults so no need to provide
// 'fix_data' => [],
// 'units' => 'metric',
// 'pace' => false
];
$pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options);
} catch (Exception $e) {
echo 'caught exception: '.$e->getMessage();
die();
}
// Google Static Maps API
$position_lat = $pFFA->data_mesgs['record']['position_lat'];
$position_long = $pFFA->data_mesgs['record']['position_long'];
$lat_long_combined = [];
foreach ($position_lat as $key => $value) { // Assumes every lat has a corresponding long
$lat_long_combined[] = [$position_lat[$key],$position_long[$key]];
}
$delta = 0.0001;
do {
$RDP_LatLng_coord = simplify_RDP($lat_long_combined, $delta); // Simplify the array of coordinates using the Ramer-Douglas-Peucker algorithm.
$delta += 0.0001; // Rough accuracy somewhere between 4m and 12m depending where in the World coordinates are, source http://en.wikipedia.org/wiki/Decimal_degrees
$polylineEncoder = new PolylineEncoder(); // Create an encoded string to pass as the path variable for the Google Static Maps API
foreach ($RDP_LatLng_coord as $RDP) {
$polylineEncoder->addPoint($RDP[0], $RDP[1]);
}
$map_encoded_polyline = $polylineEncoder->encodedString();
$map_string = '&path=color:red%7Cenc:'.$map_encoded_polyline;
} while (strlen($map_string) > 1800); // Google Map web service URL limit is 2048 characters. 1800 is arbitrary attempt to stay under 2048
$LatLng_start = implode(',', $lat_long_combined[0]);
$LatLng_finish = implode(',', $lat_long_combined[count($lat_long_combined)-1]);
$map_string .= '&markers=color:red%7Clabel:F%7C'.$LatLng_finish.'&markers=color:green%7Clabel:S%7C'.$LatLng_start;
// Google Time Zone API
$date = new DateTime('now', new DateTimeZone('UTC'));
$date_s = $pFFA->data_mesgs['session']['start_time'];
$url_tz = 'https://maps.googleapis.com/maps/api/timezone/json?location='.$LatLng_start.'&timestamp='.$date_s.'&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE';
$result = file_get_contents($url_tz);
$json_tz = json_decode($result);
if ($json_tz->status == 'OK') {
$date_s = $date_s + $json_tz->rawOffset + $json_tz->dstOffset;
} else {
$json_tz->timeZoneName = 'Error';
}
$date->setTimestamp($date_s);
// Google Geocoding API
$location = 'Error';
$url_coord = 'https://maps.googleapis.com/maps/api/geocode/json?latlng='.$LatLng_start.'&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE';
$result = file_get_contents($url_coord);
$json_coord = json_decode($result);
if ($json_coord->status == 'OK') {
foreach ($json_coord->results[0]->address_components as $addressPart) {
if ((in_array('locality', $addressPart->types)) && (in_array('political', $addressPart->types))) {
$city = $addressPart->long_name;
} elseif ((in_array('administrative_area_level_1', $addressPart->types)) && (in_array('political', $addressPart->types))) {
$state = $addressPart->short_name;
} elseif ((in_array('country', $addressPart->types)) && (in_array('political', $addressPart->types))) {
$country = $addressPart->long_name;
}
}
$location = $city.', '.$state.', '.$country;
}
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>phpFITFileAnalysis demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div class="jumbotron">
<div class="container">
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-6">
<dl class="dl-horizontal">
<dt>File: </dt>
<dd><?php echo $file; ?></dd>
<dt>Device: </dt>
<dd><?php echo $pFFA->manufacturer() . ' ' . $pFFA->product(); ?></dd>
<dt>Sport: </dt>
<dd><?php echo $pFFA->sport(); ?></dd>
</dl>
</div>
<div class="col-md-6">
<dl class="dl-horizontal">
<dt>Recorded: </dt>
<dd>
<?php
echo $date->format('D, d-M-y @ g:ia');
?>
</dd>
<dt>Duration: </dt>
<dd><?php echo gmdate('H:i:s', $pFFA->data_mesgs['session']['total_elapsed_time']); ?></dd>
<dt>Distance: </dt>
<dd><?php echo max($pFFA->data_mesgs['record']['distance']); ?> km</dd>
</dl>
</div>
</div>
<div class="col-md-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Messages</h3>
</div>
<div class="panel-body">
<?php
// Output all the Messages read in the FIT file.
foreach ($pFFA->data_mesgs as $mesg_key => $mesg) {
if ($mesg_key == 'record') {
echo '<strong><mark><u>';
}
echo $mesg_key.'<br>';
if ($mesg_key == 'record') {
echo '</u></mark></strong>';
}
}
?>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Record Fields</h3>
</div>
<div class="panel-body">
<?php
// Output all the Fields found in Record messages within the FIT file.
foreach ($pFFA->data_mesgs['record'] as $mesg_key => $mesg) {
if ($mesg_key == 'speed' || $mesg_key == 'heart_rate') {
echo '<strong><mark><u>';
}
echo $mesg_key.'<br>';
if ($mesg_key == 'speed' || $mesg_key == 'heart_rate') {
echo '</strong></mark></u>';
}
}
?>
</div>
</div>
</div>
<div class="col-md-10">
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><a href="http://www.flotcharts.org/" target="_blank"><i class="fa fa-pie-chart"></i> Flot Charts</a> <small><i class="fa fa-long-arrow-left"></i> click</small></h3>
</div>
<div class="panel-body">
<div class="col-md-12">
<div id="speed" style="width:100%; height:75px; margin-bottom:8px"></div>
<div id="heart_rate" style="width:100%; height:75px; margin-bottom:8px"></div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-map-marker"></i> Google Map</h3>
</div>
<div class="panel-body">
<div id="gmap" style="padding-bottom:20px; text-align:center;">
<strong>Google Geocoding API: </strong><?php echo $location; ?><br>
<strong>Google Time Zone API: </strong><?php echo $json_tz->timeZoneName; ?><br><br>
<img src="https://maps.googleapis.com/maps/api/staticmap?size=640x480&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE<?php echo $map_string; ?>" alt="Google map" border="0">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-12"><hr></div>
<div class="col-md-10 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-bug"></i> Debug Information</h3>
</div>
<div class="panel-body">
<?php $pFFA->showDebugInfo(); ?>
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
<script language="javascript" type="text/javascript" src="js/jquery.flot.min.js"></script>
<script type="text/javascript">
$(document).ready( function() {
var speed_options = {
lines: { show: true, fill: true, fillColor: "rgba(11, 98, 164, 0.4)", lineWidth: 1 },
points: { show: false },
xaxis: {
show: false
},
yaxis: {
max: 35,
tickFormatter: function(label, series) {
return label + ' kmh';
}
},
grid: {
borderWidth: {
top: 0,
right: 0,
bottom: 0,
left: 0
}
}
};
var speed = {
'color': 'rgba(11, 98, 164, 0.8)',
'data': [
<?php
$tmp = [];
foreach ($pFFA->data_mesgs['record']['speed'] as $key => $value) {
$tmp[] = '['.$key.', '.$value.']';
}
echo implode(', ', $tmp);
?>
]
};
var heart_rate_options = {
lines: { show: true, fill: true, fillColor: 'rgba(255, 0, 0, .4)', lineWidth: 1 },
points: { show: false },
xaxis: {
show: false
},
yaxis: {
min: 80,
tickFormatter: function(label, series) {
return label + ' bpm';
}
},
grid: {
borderWidth: {
top: 0,
right: 0,
bottom: 0,
left: 0
}
}
};
var heart_rate = {
'color': 'rgba(255, 0, 0, 0.8)',
'data': [
<?php
unset($tmp);
$tmp = [];
foreach ($pFFA->data_mesgs['record']['heart_rate'] as $key => $value) {
$tmp[] = '['.$key.', '.$value.']';
}
echo implode(', ', $tmp);
?>
]
};
$.plot('#speed', [speed], speed_options);
$.plot('#heart_rate', [heart_rate], heart_rate_options);
});
</script>
</body>
</html>

@ -0,0 +1,490 @@
<?php
/**
* Demonstration of the phpFITFileAnalysis class using Twitter Bootstrap framework
* https://github.com/adriangibbons/phpFITFileAnalysis
*
* If you find this useful, feel free to drop me a line at Adrian.GitHub@gmail.com
*/
require __DIR__ . '/../src/phpFITFileAnalysis.php';
try {
$file = '/fit_files/power-analysis.fit';
$options = [
// 'fix_data' => ['all'],
// 'units' => ['metric']
];
$pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options);
// Google Time Zone API
$date = new DateTime('now', new DateTimeZone('UTC'));
$date_s = $pFFA->data_mesgs['session']['start_time'];
$url_tz = "https://maps.googleapis.com/maps/api/timezone/json?location=".reset($pFFA->data_mesgs['record']['position_lat']).','.reset($pFFA->data_mesgs['record']['position_long'])."&timestamp=".$date_s."&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE";
$result = file_get_contents("$url_tz");
$json_tz = json_decode($result);
if ($json_tz->status == "OK") {
$date_s = $date_s + $json_tz->rawOffset + $json_tz->dstOffset;
}
$date->setTimestamp($date_s);
$ftp = 329;
$hr_metrics = $pFFA->hrMetrics(52, 185, 172, 'male');
$power_metrics = $pFFA->powerMetrics($ftp);
$criticalPower = $pFFA->criticalPower([2,3,5,10,30,60,120,300,600,1200,3600,7200,10800,18000]);
$power_histogram = $pFFA->powerHistogram();
$power_table = $pFFA->powerPartioned($ftp);
$power_pie_chart = $pFFA->partitionData('power', $pFFA->powerZones($ftp), true, false);
$quad_plot = $pFFA->quadrantAnalysis(0.175, $ftp);
} catch (Exception $e) {
echo 'caught exception: '.$e->getMessage();
die();
}
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>phpFITFileAnalysis demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div class="jumbotron">
<div class="container">
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
</div>
</div>
<div class="container">
<div class="col-md-6">
<dl class="dl-horizontal">
<dt>File: </dt>
<dd><?php echo $file; ?></dd>
<dt>Device: </dt>
<dd><?php echo $pFFA->manufacturer() . ' ' . $pFFA->product(); ?></dd>
<dt>Sport: </dt>
<dd><?php echo $pFFA->sport(); ?></dd>
</dl>
</div>
<div class="col-md-6">
<dl class="dl-horizontal">
<dt>Recorded: </dt>
<dd><?php echo $date->format('D, d-M-y @ g:ia'); ?>
</dd>
<dt>Duration: </dt>
<dd><?php echo gmdate('H:i:s', $pFFA->data_mesgs['session']['total_elapsed_time']); ?></dd>
<dt>Distance: </dt>
<dd><?php echo max($pFFA->data_mesgs['record']['distance']); ?> km</dd>
</dl>
</div>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-tachometer"></i> Metrics</h3></a>
</div>
<div class="panel-body">
<div class="col-md-5 col-md-offset-1">
<h4>Power</h4>
<?php
foreach ($power_metrics as $key => $value) {
echo "$key: $value<br>";
}
?>
</div>
<div class="col-md-5">
<h4>Heart Rate</h4>
<?php
foreach ($hr_metrics as $key => $value) {
echo "$key: $value<br>";
}
?>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-line-chart"></i> Critical Power</h3></a>
</div>
<div class="panel-body">
<div id="criticalPower" style="width:100%; height:300px"></div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Power Distribution (histogram)</h3>
</div>
<div class="panel-body">
<div id="power_distribution" style="width:100%; height:300px"></div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-pie-chart"></i> Power Zones</h3>
</div>
<div class="panel-body">
<div class="col-md-4 col-md-offset-1">
<table id="power_zones_table" class="table table-bordered table-striped">
<thead>
<th>Zone</th>
<th>Zone range</th>
<th>% in zone</th>
</thead>
<tbody>
<?php
$i = 1;
foreach ($power_table as $key => $value) {
echo '<tr id="'.number_format($value, 1, '-', '').'">';
echo '<td>'.$i++.'</td><td>'.$key.' w</td><td>'.$value.' %</td>';
echo '</tr>';
}
?>
</tr>
</tbody>
</table>
</div>
<div class="col-md-4 col-md-offset-1">
<div id="power_pie_chart" style="width:100%; height:250px"></div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-line-chart"></i> Quadrant Analysis <small>Circumferential Pedal Velocity (x-axis) vs Average Effective Pedal Force (y-axis)</small></h3>
</div>
<div class="panel-body">
<div id="quadrant_analysis" style="width:100%; height:600px"></div>
</div>
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
<script language="javascript" type="text/javascript" src="js/jquery.flot.min.js"></script>
<script language="javascript" type="text/javascript" src="js/jquery.flot.pie.min.js"></script>
<script type="text/javascript">
$(document).ready( function() {
var criticalPower_options = {
lines: { show: true, fill: true, fillColor: "rgba(11, 98, 164, 0.5)", lineWidth: 1 },
points: { show: true },
xaxis: {
ticks: [2,3,5,10,30,60,120,300,600,1200,3600,7200,10800,18000],
transform: function (v) { return Math.log(v); },
inverseTransform: function (v) { return Math.exp(v); },
tickFormatter: function(label, series) {
var hours = parseInt( label / 3600 ) % 24;
var minutes = parseInt( label / 60 ) % 60;
var seconds = label % 60;
var result = (hours > 0 ? hours + "h" : (minutes > 0 ? minutes + "m" : seconds + 's'));
return result;
}
},
yaxis: {
tickFormatter: function(label, series) {
if(label > 0) return label.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' w';
else return '';
}
},
grid: {
hoverable: true,
borderWidth: {
top: 0,
right: 0,
bottom: 0,
left: 0
}
}
};
var criticalPower = {
'color': 'rgba(11, 98, 164, 1)',
'data': [
<?php
foreach ($criticalPower as $key => $value) {
echo '['.$key.', '.$value.'], ';
}
?>
]
};
var markings = [{ color: "rgba(203, 75, 75, 1)", lineWidth: 2, xaxis: { from: <?php echo $power_metrics['Normalised Power']; ?>, to: <?php echo $power_metrics['Normalised Power']; ?> } }];
var power_distribution_options = {
points: { show: false },
xaxis: {
show: true,
min: 0,
tickSize: 100,
tickFormatter: function(label, series) { return label.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' w'; }
},
yaxis: {
min: 0,
label: 'time in zone',
tickSize: 300,
tickFormatter: function(label, series) {
if(label == 0) return "";
return (label / 60) + ' min';
}
},
grid: {
borderWidth: {
top: 0,
right: 0,
bottom: 0,
left: 0
},
markings: markings
},
legend: { show: false }
};
var power_distribution = {
'color': 'rgba(77, 167, 77, 0.8)',
bars: { show: true, zero: false, barWidth: 25, fillColor: "rgba(77, 167, 77, 0.5)", lineWidth: 1 },
'data': [
<?php
foreach ($power_histogram as $key => $value) {
echo '['.$key.', '.$value.'], ';
}
?>
]
};
var power_pie_chart_options = {
series: {
pie: {
radius: 1,
show: true,
label: {
show: true,
radius: 3/4,
formatter: labelFormatter
}
}
},
grid: { hoverable: true },
legend: { show: false }
};
function labelFormatter(label, series) {
return "<div style='font-size:8pt; text-align:center; padding:2px; color:#333; border-radius: 5px; background-color: #fafafa; border: 1px solid #ddd;'><strong>" + label + "</strong><br/>" + series.data[0][1] + "%</div>";
}
var power_pie_chart = [
{
label: "Active Recovery",
data: <?php echo $power_pie_chart[0]; ?>,
"color": "rgba(217, 83, 79, 0.2)"
},
{
label: "Endurance",
data: <?php echo $power_pie_chart[1]; ?>,
"color": "rgba(217, 83, 79, 0.35)"
},
{
label: "Tempo",
data: <?php echo $power_pie_chart[2]; ?>,
"color": "rgba(217, 83, 79, 0.5)"
},
{
label: "Threshold",
data: <?php echo $power_pie_chart[3]; ?>,
"color": "rgba(217, 83, 79, 0.65)"
},
{
label: "VO2max",
data: <?php echo $power_pie_chart[4]; ?>,
"color": "rgba(217, 83, 79, 0.7)"
},
{
label: "Anaerobic",
data: <?php echo $power_pie_chart[5]; ?>,
"color": "rgba(217, 83, 79, 0.85)"
},
{
label: "Neuromuscular",
data: <?php echo $power_pie_chart[6]; ?>,
"color": "rgba(217, 83, 79, 1)"
}
];
$("<div id='tooltip_bg'></div>").css({
position: "absolute",
display: "none",
"text-align": "center",
"-moz-border-radius": "5px",
"-webkit-border-radius": "5px",
"border-radius": "5px",
"border": "2px solid #fff",
padding: "3px 7px",
"font-size": "12px",
"color": "#fff",
"background-color": "#fff"
}).appendTo("body");
$("<div id='tooltip'></div>").css({
position: "absolute",
display: "none",
"text-align": "center",
"-moz-border-radius": "5px",
"-webkit-border-radius": "5px",
"border-radius": "5px",
"border": "2px solid",
padding: "3px 7px",
"font-size": "12px",
"color": "#555"
}).appendTo("body");
$("#criticalPower").bind("plothover", function (event, pos, item) {
if (item) {
var x = item.datapoint[0].toFixed(2),
y = item.datapoint[1].toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
var currentColor = item.series.color;
var lastComma = currentColor.lastIndexOf(',');
var newColor = currentColor.slice(0, lastComma + 1) + "0.1)";
$("#tooltip").html('<strong>' + item.series.xaxis.ticks[item.dataIndex].label + '</strong><br>' + y + ' w')
.css({top: item.pageY-45, left: item.pageX+5, "border-color": item.series.color, "background-color": newColor })
.fadeIn(200);
$("#tooltip_bg").html('<strong>' + item.series.xaxis.ticks[item.dataIndex].label + '</strong><br>' + y + ' w')
.css({top: item.pageY-45, left: item.pageX+5 })
.fadeIn(200);
} else {
$("#tooltip").hide();
$("#tooltip_bg").hide();
}
});
$.plot('#criticalPower', [criticalPower], criticalPower_options);
var plot_pd = $.plot('#power_distribution', [power_distribution], power_distribution_options);
var o = plot_pd.pointOffset({ x: <?php echo $power_metrics['Normalised Power']; ?>, y: plot_pd.height() });
$("#power_distribution").append("<span style='background-color: #fafafa; top: 12px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left:" + (o.left + 6) + "px'><strong>normalised power</strong><br><?php echo $power_metrics['Normalised Power']; ?> w</span>");
$.plot('#power_pie_chart', power_pie_chart, power_pie_chart_options);
$("#power_pie_chart").bind("plothover", function (event, pos, obj) {
if (!obj) {
$("#power_zones_table tr").removeClass("danger");
return;
}
$("#power_zones_table tr").removeClass("danger");
$("#" + obj.series.data[0][1].toFixed(1).toString().replace(/\./g, '-') ).addClass("danger");
});
var quad = [<?php
$plottmp = [];
foreach ($quad_plot['plot'] as $v) {
$plottmp[] = '[' . $v[0] . ', ' . $v[1] . ']';
}
echo implode(', ', $plottmp); ?>];
var lo = [<?php
unset ($plottmp);
$plottmp = [];
foreach ($quad_plot['ftp-25w'] as $v) {
$plottmp[] = '[' . $v[0] . ', ' . $v[1] . ']';
}
echo implode(', ', $plottmp); ?>];
var at = [<?php
unset ($plottmp);
$plottmp = [];
foreach ($quad_plot['ftp'] as $v) {
$plottmp[] = '[' . $v[0] . ', ' . $v[1] . ']';
}
echo implode(', ', $plottmp); ?>];
var hi = [<?php
unset ($plottmp);
$plottmp = [];
foreach ($quad_plot['ftp+25w'] as $v) {
$plottmp[] = '[' . $v[0] . ', ' . $v[1] . ']';
}
echo implode(', ', $plottmp); ?>];
var markings = [
{
color: "black",
lineWidth: 1,
xaxis: {
from: <?php echo $quad_plot['cpv_threshold']; ?>,
to: <?php echo $quad_plot['cpv_threshold']; ?>
}
},
{
color: "black",
lineWidth: 1,
yaxis: {
from: <?php echo $quad_plot['aepf_threshold']; ?>,
to: <?php echo $quad_plot['aepf_threshold']; ?>
}
}
];
var quadrant_analysis_options = {
xaxis: {
label: 'circumferential pedal velocity',
tickFormatter: function(label, series) { return label + ' m/s'; }
},
yaxis: {
max: 400,
label: 'average effective pedal force',
tickSize: 50,
tickFormatter: function(label, series) {
if(label == 0) return "";
return label + ' N';
}
},
grid: {
borderWidth: {
top: 0,
right: 0,
bottom: 0,
left: 0
},
markings: markings
},
legend: { show: false }
};
var plot_qa = $.plot($("#quadrant_analysis"), [
{
data : quad,
points : { show: true, radius: 0.25, fill : true, fillColor: "#058DC7" }
},
{
data : at,
color: "blue",
lines: { show: true, lineWidth: 0.5 }
},
{
data : lo,
color: "red",
lines: { show: true, lineWidth: 0.5 }
},
{
data : hi,
color: "green",
lines: { show: true, lineWidth: 0.5 }
}
], quadrant_analysis_options);
$("#quadrant_analysis").append("<span style='background-color: #fafafa; top: " + (plot_qa.height() / 2 - 40) + "px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left: " + (plot_qa.width() - 140) + "px'><strong>High Force / High Velocity</strong><br><?php echo $quad_plot['quad_percent']['hf_hv']; ?> %</span>");
$("#quadrant_analysis").append("<span style='background-color: #fafafa; top: " + (plot_qa.height() / 2 - 40) + "px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left: 50px'><strong>High Force / Low Velocity</strong><br><?php echo $quad_plot['quad_percent']['hf_lv']; ?> %</span>");
$("#quadrant_analysis").append("<span style='background-color: #fafafa; top: " + (plot_qa.height() / 2 + 15) + "px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left: 50px'><strong>Low Force / Low Velocity</strong><br><?php echo $quad_plot['quad_percent']['lf_lv']; ?> %</span>");
$("#quadrant_analysis").append("<span style='background-color: #fafafa; top: " + (plot_qa.height() / 2 + 15) + "px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left: " + (plot_qa.width() - 140) + "px'><strong>Low Force / High Velocity</strong><br><?php echo $quad_plot['quad_percent']['lf_hv']; ?> %</span>");
});
</script>
</body>
</html>

@ -0,0 +1,356 @@
<?php
/**
* Demonstration of the phpFITFileAnalysis class using Twitter Bootstrap framework
* https://github.com/adriangibbons/phpFITFileAnalysis
*
* If you find this useful, feel free to drop me a line at Adrian.GitHub@gmail.com
*/
require __DIR__ . '/../src/phpFITFileAnalysis.php';
try {
$file = '/fit_files/power-analysis.fit';
$options = [
// 'fix_data' => ['all'],
// 'units' => ['metric']
];
$pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options);
// Google Time Zone API
$date = new DateTime('now', new DateTimeZone('UTC'));
$date_s = $pFFA->data_mesgs['session']['start_time'];
$url_tz = "https://maps.googleapis.com/maps/api/timezone/json?location=".reset($pFFA->data_mesgs['record']['position_lat']).','.reset($pFFA->data_mesgs['record']['position_long'])."&timestamp=".$date_s."&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE";
$result = file_get_contents("$url_tz");
$json_tz = json_decode($result);
if ($json_tz->status == "OK") {
$date_s = $date_s + $json_tz->rawOffset + $json_tz->dstOffset;
}
$date->setTimestamp($date_s);
$crank_length = 0.175;
$ftp = 329;
$selected_cadence = 90;
$json = $pFFA->getJSON($crank_length, $ftp, ['all'], $selected_cadence);
} catch (Exception $e) {
echo 'caught exception: '.$e->getMessage();
die();
}
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>phpFITFileAnalysis demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="css/dc.css">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div class="jumbotron">
<div class="container">
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
</div>
</div>
<div class="container">
<div class="col-md-6">
<dl class="dl-horizontal">
<dt>File: </dt>
<dd><?php echo $file; ?></dd>
<dt>Device: </dt>
<dd><?php echo $pFFA->manufacturer() . ' ' . $pFFA->product(); ?></dd>
<dt>Sport: </dt>
<dd><?php echo $pFFA->sport(); ?></dd>
</dl>
</div>
<div class="col-md-6">
<dl class="dl-horizontal">
<dt>Recorded: </dt>
<dd><?php echo $date->format('D, d-M-y @ g:ia'); ?>
</dd>
<dt>Duration: </dt>
<dd><?php echo gmdate('H:i:s', $pFFA->data_mesgs['session']['total_elapsed_time']); ?></dd>
<dt>Distance: </dt>
<dd><?php echo max($pFFA->data_mesgs['record']['distance']); ?> km</dd>
</dl>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<i class="fa fa-bar-chart"></i> Quadrant Analysis
<button id="reset-button" type="button" class="btn btn-primary btn-xs pull-right">Reset all filters</button>
</h3>
</div>
<div class="panel-body">
<div id="quadrant-analysis-scatter-chart"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Google Map</h3>
</div>
<div class="panel-body">
<div class="embed-responsive embed-responsive-4by3">
<div id="google-map" class="embed-responsive-item"></div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Laps</h3>
</div>
<div class="panel-body">
<div id="lap-row-chart"></div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Cadence histogram</h3>
</div>
<div class="panel-body">
<div id="cad-bar-chart"></div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Heart Rate histogram</h3>
</div>
<div class="panel-body">
<div id="hr-bar-chart"></div>
</div>
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
<script type="text/javascript" src="js/d3.js"></script>
<script type="text/javascript" src="js/crossfilter.js"></script>
<script type="text/javascript" src="js/dc.js"></script>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?libraries=geometry&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE"></script>
<script type="text/javascript">
$(function(){
var ride_data = <?php echo $json; ?>;
console.log(ride_data);
var segmentOverlay;
var mapOptions = {
zoom: 8
};
var map = new google.maps.Map(document.getElementById('google-map'), mapOptions);
var routeCoordinates = [];
$.each(ride_data.data, function(k, v) {
if(v.position_lat !== null && v.position_long !== null) {
routeCoordinates.push(new google.maps.LatLng(v.position_lat, v.position_long));
}
});
var routePath = new google.maps.Polyline({
path: routeCoordinates,
geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 0.8,
strokeWeight: 3
});
routePath.setMap(map);
var bounds = new google.maps.LatLngBounds();
for (var i = 0; i < routeCoordinates.length; i++) {
bounds.extend(routeCoordinates[i]);
}
map.fitBounds(bounds);
google.maps.event.addDomListener(window, "resize", function() {
var center = map.getCenter();
google.maps.event.trigger(map, "resize");
map.setCenter(center);
});
function updateOverlay(data) {
if(typeof segmentOverlay !== 'undefined') {
segmentOverlay.setMap(null);
}
var segmentCoords = [];
$.each(data, function(k, v) {
if(v.position_lat !== null && v.position_long !== null) {
segmentCoords.push(new google.maps.LatLng(v.position_lat, v.position_long));
}
});
segmentOverlay = new google.maps.Polyline({
path: segmentCoords,
geodesic: true,
strokeColor: '#0000FF',
strokeOpacity: 0.8,
strokeWeight: 3
});
segmentOverlay.setMap(map);
var bounds = new google.maps.LatLngBounds();
for (var i = 0; i < segmentCoords.length; i++) {
bounds.extend(segmentCoords[i]);
}
map.fitBounds(bounds);
}
var qaScatterChart = dc.seriesChart("#quadrant-analysis-scatter-chart"),
lapRowChart = dc.rowChart("#lap-row-chart"),
cadBarChart = dc.barChart("#cad-bar-chart"),
hrBarChart = dc.barChart("#hr-bar-chart");
var ndx,
tsDim, latlngDim, qaDim, lapDim, cadDim, hrDim,
qaGrp, lapGrp, cadGrp, hrGrp;
ndx = crossfilter(ride_data.data);
tsDim = ndx.dimension(function(d) {return d.timestamp;});
latlngDim = ndx.dimension(function(d) {return [d.position_lat, d.position_long];});
qaDim = ndx.dimension(function(d) {return [d.cpv, d.aepf, d.lap];});
qaGrp = qaDim.group().reduceSum(function(d) { return d.lap; });
lapDim = ndx.dimension(function(d) {return d.lap;});
lapGrp = lapDim.group();
cadDim = ndx.dimension(function(d) {return d.cadence;});
cadGrp = cadDim.group();
hrDim = ndx.dimension(function(d) {return d.heart_rate;});
hrGrp = hrDim.group();
// Quadrant Analysis chart
var subChart = function(c) {
return dc.scatterPlot(c)
.symbolSize(5)
.highlightedSize(8)
};
qaScatterChart
.width(550)
.height(388)
.chart(subChart)
.x(d3.scale.linear().domain([0,2.5]))
.brushOn(false)
.yAxisLabel("Average Effective Pedal Force (N)")
.xAxisLabel("Circumferential Pedal Velocity (m/s)")
.elasticY(true)
.dimension(qaDim)
.group(qaGrp)
.seriesAccessor(function(d) {return "Lap: " + d.key[2];})
.keyAccessor(function(d) {return d.key[0];})
.valueAccessor(function(d) {return d.key[1];})
.legend(dc.legend().x(450).y(50).itemHeight(13).gap(5).horizontal(1).legendWidth(70).itemWidth(70));
qaScatterChart.margins().left += 20;
qaScatterChart.margins().bottom += 10;
var hght = (lapGrp.size() * 40) > 76 ? lapGrp.size() * 40 : 76;
// Lap chart
lapRowChart
.width(375).height(hght)
.dimension(lapDim)
.group(lapGrp)
.elasticX(true)
.gap(2)
.label(function(d) {
var hours = parseInt(d.value / 3600) % 24;
var minutes = parseInt(d.value / 60 ) % 60;
var seconds = d.value % 60;
return 'Lap ' + d.key + ' (' + ((hours > 0 ? hours + 'h ' : '') + (minutes < 10 ? "0" + minutes : minutes) + "m " + (seconds < 10 ? "0" + seconds : seconds) + 's)');
});
// Cadence chart
cadBarChart
.width(375).height(150)
.dimension(cadDim)
.group(cadGrp)
.x(d3.scale.linear().domain([40,cadDim.top(1)[0].cadence]))
.elasticY(true);
cadBarChart.margins().left = 45;
cadBarChart.yAxis()
.tickFormat(function(d) {
var hours = parseInt(d / 3600) % 24;
var minutes = parseInt(d / 60 ) % 60;
var seconds = d % 60;
return (hours > 0 ? hours + ':' : '') + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds);
})
.ticks(4);
// HR chart
hrBarChart
.width(375).height(150)
.dimension(hrDim)
.group(hrGrp)
.x(d3.scale.linear().domain([hrDim.bottom(1)[0].heart_rate,hrDim.top(1)[0].heart_rate]))
.elasticY(true);
hrBarChart.margins().left = 45;
hrBarChart.yAxis()
.tickFormat(function(d) {
var hours = parseInt(d / 3600) % 24;
var minutes = parseInt(d / 60 ) % 60;
var seconds = d % 60;
return (hours > 0 ? hours + ':' : '') + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds);
})
.ticks(4);
dc.renderAll();
lapRowChart.on('filtered', function(chart, filter){
updateOverlay(tsDim.bottom(Infinity));
});
qaScatterChart.on('renderlet', function(chart) {
var horizontal_line = [{x: chart.x().range()[0], y: chart.y()(ride_data.aepf_threshold)},
{x: chart.x().range()[1], y: chart.y()(ride_data.aepf_threshold)}];
var vertical_line = [{x: chart.x()(ride_data.cpv_threshold), y: chart.y().range()[0]},
{x: chart.x()(ride_data.cpv_threshold), y: chart.y().range()[1]}];
var line = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
// .interpolate('linear');
var path = chart.select('g.chart-body').selectAll('path.aepf_threshold').data([horizontal_line]);
path.enter().append('path').attr('class', 'aepf_threshold').attr('stroke', 'black');
path.attr('d', line);
var path2 = chart.select('g.chart-body').selectAll('path.cpv_threshold').data([vertical_line]);
path2.enter().append('path').attr('class', 'cpv_threshold').attr('stroke', 'black');
path2.attr('d', line);
});
$('#reset-button').on('click', function(e) {
e.preventDefault(); // preventing default click action
dc.filterAll();
dc.redrawAll();
if(typeof segmentOverlay !== 'undefined') {
segmentOverlay.setMap(null);
}
});
});
</script>
</body>
</html>

@ -0,0 +1,185 @@
<?php
/**
* Demonstration of the phpFITFileAnalysis class using Twitter Bootstrap framework
* https://github.com/adriangibbons/phpFITFileAnalysis
*
* If you find this useful, feel free to drop me a line at Adrian.GitHub@gmail.com
*/
require __DIR__ . '/../src/phpFITFileAnalysis.php';
try {
$file = '/fit_files/swim.fit';
$options = [
// 'fix_data' => [],
'units' => 'raw',
// 'pace' => false
];
$pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options);
} catch (Exception $e) {
echo 'caught exception: '.$e->getMessage();
die();
}
$units = 'm';
$pool_length = $pFFA->data_mesgs['session']['pool_length'];
$total_distance = number_format($pFFA->data_mesgs['record']['distance']);
if ($pFFA->enumData('display_measure', $pFFA->data_mesgs['session']['pool_length_unit']) == 'statute') {
$pool_length = round($pFFA->data_mesgs['session']['pool_length'] * 1.0936133);
$total_distance = number_format($pFFA->data_mesgs['record']['distance'] * 1.0936133);
$units = 'yd';
}
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>phpFITFileAnalysis demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div class="jumbotron">
<div class="container">
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
</div>
</div>
<div class="container">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> FIT File info</h3>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>File: </dt>
<dd><?php echo $file; ?></dd>
<dt>Device: </dt>
<dd><?php echo $pFFA->manufacturer() . ' ' . $pFFA->product(); ?></dd>
<dt>Sport: </dt>
<dd><?php echo $pFFA->sport(); ?></dd>
<dt>Pool length: </dt>
<dd><?php echo $pool_length.' '.$units; ?></dd>
<dt>Duration: </dt>
<dd><?php echo gmdate('H:i:s', $pFFA->data_mesgs['session']['total_elapsed_time']); ?></dd>
<dt>Total distance: </dt>
<dd><?php echo $total_distance.' '.$units; ?></dd>
</dl>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Lap Time vs. Number of Strokes</h3>
</div>
<div class="panel-body">
<div id="lap_times" style="width:100%; height:200px; margin-bottom:8px"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-tags"></i> Length Message fields</h3>
</div>
<div class="panel-body">
<table class="table table-condensed table-striped">
<thead>
<th>Length</th>
<th>Time (min:sec)</th>
<th># Strokes</th>
<th>Stroke</th>
</thead>
<tbody>
<?php
$lengths = count($pFFA->data_mesgs['length']['total_timer_time']);
$active_length = 0;
for ($i=0; $i<$lengths; $i++) {
$min = floor($pFFA->data_mesgs['length']['total_timer_time'][$i] / 60);
$sec = number_format($pFFA->data_mesgs['length']['total_timer_time'][$i] - ($min*60), 1);
$dur = $min.':'.$sec;
if ($pFFA->enumData('length_type', $pFFA->data_mesgs['length']['length_type'][$i]) == 'active') {
echo '<tr>';
echo '<td>'.($i+1).'</td>';
echo '<td>'.$dur.'</td>';
echo '<td>'.$pFFA->data_mesgs['length']['total_strokes'][$i].'</td>';
echo '<td>'.$pFFA->enumData('swim_stroke', $pFFA->data_mesgs['length']['swim_stroke'][$active_length]).'</td>';
echo '<td></td>';
echo '</tr>';
$active_length++;
} else {
echo '<tr class="danger">';
echo '<td>'.($i+1).'</td>';
echo '<td>'.$dur.'</td>';
echo '<td>-</td>';
echo '<td>Rest</td>';
echo '</tr>';
}
}
?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
<script language="javascript" type="text/javascript" src="js/jquery.flot.min.js"></script>
<script type="text/javascript">
$(document).ready( function() {
var chart_options = {
xaxis: {
show: false
},
yaxes: [ { transform: function (v) { return -v; }, inverseTransform: function (v) { return -v; }, tickFormatter: function(label, series) { return label + ' s'; } },
{ alignTicksWithAxis: 1, position: "right", } ],
grid: {
borderWidth: {
top: 0,
right: 0,
bottom: 0,
left: 0
}
}
};
var lap_times = {
'color': 'rgba(255, 0, 0, 1)',
'label': 'Lap Time',
'data': [
<?php
$tmp = [];
for ($i=0; $i<$lengths; $i++) {
if ($pFFA->enumData('length_type', $pFFA->data_mesgs['length']['length_type'][$i]) == 'active') {
$tmp[] = '['.$i.', '.$pFFA->data_mesgs['length']['total_timer_time'][$i].']';
}
}
echo implode(', ', $tmp);
?>
],
lines: { show: true, fill: false, lineWidth: 2 },
points: { show: false }
};
var num_strokes = {
'color': 'rgba(11, 98, 164, 0.5)',
'label': 'Number of Strokes',
'data': [
<?php
$tmp = [];
for ($i=0; $i<$lengths; $i++) {
if ($pFFA->enumData('length_type', $pFFA->data_mesgs['length']['length_type'][$i]) == 'active') {
$tmp[] = '['.$i.', '.$pFFA->data_mesgs['length']['total_strokes'][$i].']';
}
}
echo implode(', ', $tmp);
?>
],
bars: { show: true, fill: true, fillColor: "rgba(11, 98, 164, 0.3)", lineWidth: 1 },
points: { show: false },
yaxis: 2
};
$.plot('#lap_times', [lap_times, num_strokes], chart_options);
});
</script>
</body>
</html>

@ -0,0 +1,92 @@
<?php
error_reporting(E_ALL);
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
require __DIR__ . '/../src/phpFITFileAnalysis.php';
}
class BasicTest extends PHPUnit_Framework_TestCase
{
private $base_dir;
private $demo_files = [];
private $valid_files = ['mountain-biking.fit', 'power-analysis.fit', 'road-cycling.fit', 'swim.fit'];
public function setUp()
{
$this->base_dir = __DIR__ . '/../demo/fit_files/';
}
public function testDemoFilesExist()
{
$this->demo_files = array_values(array_diff(scandir($this->base_dir), array('..', '.')));
sort($this->demo_files);
sort($this->valid_files);
$this->assertEquals($this->valid_files, $this->demo_files);
var_dump($this->demo_files);
}
/**
* @expectedException Exception
*/
public function testEmptyFilepath()
{
$pFFA = new adriangibbons\phpFITFileAnalysis('');
}
/**
* @expectedException Exception
*/
public function testFileDoesntExist()
{
$pFFA = new adriangibbons\phpFITFileAnalysis('file_doesnt_exist.fit');
}
/**
* @expectedException Exception
*/
public function testInvalidFitFile()
{
$file_path = $this->base_dir . '../composer.json';
$pFFA = new adriangibbons\phpFITFileAnalysis($file_path);
}
public function testDemoFileBasics()
{
foreach($this->demo_files as $filename) {
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $filename);
$this->assertGreaterThan(0, $pFFA->data_mesgs['activity']['timestamp'], 'No Activity timestamp!');
if (isset($pFFA->data_mesgs['record'])) {
$this->assertGreaterThan(0, count($pFFA->data_mesgs['record']['timestamp']), 'No Record timestamps!');
// Check if distance from record messages is +/- 2% of distance from session message
if (is_array($pFFA->data_mesgs['record']['distance'])) {
$distance_difference = abs(end($pFFA->data_mesgs['record']['distance']) - $pFFA->data_mesgs['session']['total_distance'] / 1000);
$this->assertLessThan(0.02 * end($pFFA->data_mesgs['record']['distance']), $distance_difference, 'Session distance should be similar to last Record distance');
}
// Look for big jumps in latitude and longitude
if (isset($pFFA->data_mesgs['record']['position_lat']) && is_array($pFFA->data_mesgs['record']['position_lat'])) {
foreach ($pFFA->data_mesgs['record']['position_lat'] as $key => $value) {
if (isset($pFFA->data_mesgs['record']['position_lat'][$key - 1])) {
if (abs($pFFA->data_mesgs['record']['position_lat'][$key - 1] - $pFFA->data_mesgs['record']['position_lat'][$key]) > 1) {
$this->assertTrue(false, 'Too big a jump in latitude');
}
}
}
}
if (isset($pFFA->data_mesgs['record']['position_long']) && is_array($pFFA->data_mesgs['record']['position_long'])) {
foreach ($pFFA->data_mesgs['record']['position_long'] as $key => $value) {
if (isset($pFFA->data_mesgs['record']['position_long'][$key - 1])) {
if (abs($pFFA->data_mesgs['record']['position_long'][$key - 1] - $pFFA->data_mesgs['record']['position_long'][$key]) > 1) {
$this->assertTrue(false, 'Too big a jump in longitude');
}
}
}
}
}
}
}
}

@ -0,0 +1,33 @@
<?php
error_reporting(E_ALL);
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
require __DIR__ . '/../src/phpFITFileAnalysis.php';
}
class EnumDataTest extends PHPUnit_Framework_TestCase
{
private $base_dir;
private $filename = 'swim.fit';
private $pFFA;
public function setUp()
{
$this->base_dir = __DIR__ . '/../demo/fit_files/';
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
}
public function testEnumData_manufacturer()
{
$this->assertEquals('Garmin', $this->pFFA->manufacturer());
}
public function testEnumData_product()
{
$this->assertEquals('Forerunner 910XT', $this->pFFA->product());
}
public function testEnumData_sport()
{
$this->assertEquals('Swimming', $this->pFFA->sport());
}
}

@ -0,0 +1,132 @@
<?php
error_reporting(E_ALL);
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
require __DIR__ . '/../src/phpFITFileAnalysis.php';
}
class FixDataTest extends PHPUnit_Framework_TestCase
{
private $base_dir;
private $filename = 'road-cycling.fit';
private $filename2 = 'power-analysis.fit';
public function setUp()
{
$this->base_dir = __DIR__ . '/../demo/fit_files/';
}
/**
* Original road-cycling.fit before fixData() contains:
*
* record message | count()
* -----------------+--------
* timestamp | 4317
* position_lat | 4309 <- test
* position_long | 4309 <- test
* distance | 4309 <- test
* altitude | 4317
* speed | 4309 <- test
* heart_rate | 4316 <- test
* temperature | 4317
*/
public function testFixData_before()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename);
$this->assertEquals(4309, count($pFFA->data_mesgs['record']['position_lat']));
$this->assertEquals(4309, count($pFFA->data_mesgs['record']['position_long']));
$this->assertEquals(4309, count($pFFA->data_mesgs['record']['distance']));
$this->assertEquals(4309, count($pFFA->data_mesgs['record']['speed']));
$this->assertEquals(4316, count($pFFA->data_mesgs['record']['heart_rate']));
$pFFA2 = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename2);
$this->assertEquals(3043, count($pFFA2->data_mesgs['record']['cadence']));
$this->assertEquals(3043, count($pFFA2->data_mesgs['record']['power']));
}
/**
* $pFFA->data_mesgs['record']['heart_rate']
* [805987191 => 118],
* [805987192 => missing],
* [805987193 => 117]
*/
public function testFixData_hr_missing_key()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename);
$hr_missing_key = array_diff($pFFA->data_mesgs['record']['timestamp'], array_keys($pFFA->data_mesgs['record']['heart_rate']));
$this->assertEquals([3036 => 1437052792], $hr_missing_key);
}
public function testFixData_after()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => ['all']]);
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['position_lat']));
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['position_long']));
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['distance']));
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['speed']));
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['heart_rate']));
$pFFA2 = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename2, ['fix_data' => ['cadence', 'power']]);
$this->assertEquals(3043, count($pFFA2->data_mesgs['record']['cadence']));
$this->assertEquals(3043, count($pFFA2->data_mesgs['record']['power']));
}
/**
* $pFFA->data_mesgs['record']['heart_rate']
* [805987191 => 118],
* [805987192 => 117.5],
* [805987193 => 117]
*/
public function testFixData_hr_missing_key_fixed()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => ['heart_rate']]);
$this->assertEquals(117.5, $pFFA->data_mesgs['record']['heart_rate'][1437052792]);
}
public function testFixData_validate_options_pass()
{
// Positive testing
$valid_options = ['all', 'cadence', 'distance', 'heart_rate', 'lat_lon', 'speed', 'power'];
foreach($valid_options as $valid_option) {
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => [$valid_option]]);
}
}
public function testFixData_data_every_second()
{
$options = [
'fix_data' => ['speed'],
'data_every_second' => true,
'units' => 'raw',
];
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, $options);
$this->assertEquals(6847, count($pFFA->data_mesgs['record']['speed']));
}
/**
* @expectedException Exception
*/
public function testFixData_validate_options_fail()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => ['INVALID']]);
}
/**
* @expectedException Exception
*/
public function testFixData_invalid_pace_option()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['pace' => 'INVALID']);
}
/**
* @expectedException Exception
*/
public function testFixData_invalid_pace_option2()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['pace' => 123456]);
}
}

@ -0,0 +1,34 @@
<?php
error_reporting(E_ALL);
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
require __DIR__ . '/../src/phpFITFileAnalysis.php';
}
class GetJSONTest extends PHPUnit_Framework_TestCase
{
private $base_dir;
private $filename = 'power-analysis.fit';
private $pFFA;
public function setUp()
{
$this->base_dir = __DIR__ . '/../demo/fit_files/';
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
}
public function testGetJSON()
{
// getJSON() create a JSON object that contains available record message information.
$crank_length = null;
$ftp = null;
$data_required = ['timestamp', 'speed'];
$selected_cadence = 90;
$php_object = json_decode($this->pFFA->getJSON($crank_length, $ftp, $data_required, $selected_cadence));
// Assert data
$this->assertEquals('raw', $php_object->units);
$this->assertEquals(3043, count($php_object->data));
$this->assertEquals(1437474517, $php_object->data[0]->timestamp);
$this->assertEquals(1.378, $php_object->data[0]->speed);
}
}

@ -0,0 +1,52 @@
<?php
error_reporting(E_ALL);
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
require __DIR__ . '/../src/phpFITFileAnalysis.php';
}
class HRTest extends PHPUnit_Framework_TestCase
{
private $base_dir;
private $filename = 'power-analysis.fit';
private $pFFA;
public function setUp()
{
$this->base_dir = __DIR__ . '/../demo/fit_files/';
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
}
public function testHR_hrMetrics()
{
$hr_metrics = $this->pFFA->hrMetrics(50, 190, 170, 'male');
$this->assertEquals(74, $hr_metrics['TRIMPexp']);
$this->assertEquals(0.8, $hr_metrics['hrIF']);
}
public function testHR_hrPartionedHRmaximum()
{
// Calls phpFITFileAnalysis::hrZonesMax()
$hr_partioned_HRmaximum = $this->pFFA->hrPartionedHRmaximum(190);
$this->assertEquals(19.4, $hr_partioned_HRmaximum['0-113']);
$this->assertEquals(33.1, $hr_partioned_HRmaximum['114-142']);
$this->assertEquals(31.4, $hr_partioned_HRmaximum['143-161']);
$this->assertEquals(16.1, $hr_partioned_HRmaximum['162-180']);
$this->assertEquals(0, $hr_partioned_HRmaximum['181+']);
}
public function testHR_hrPartionedHRreserve()
{
// Calls phpFITFileAnalysis::hrZonesReserve()
$hr_partioned_HRreserve = $this->pFFA->hrPartionedHRreserve(50, 190);
$this->assertEquals(45.1, $hr_partioned_HRreserve['0-133']);
$this->assertEquals(5.8, $hr_partioned_HRreserve['134-140']);
$this->assertEquals(20.1, $hr_partioned_HRreserve['141-154']);
$this->assertEquals(15.9, $hr_partioned_HRreserve['155-164']);
$this->assertEquals(12.5, $hr_partioned_HRreserve['165-174']);
$this->assertEquals(0.6, $hr_partioned_HRreserve['175-181']);
$this->assertEquals(0, $hr_partioned_HRreserve['182+']);
}
}

@ -0,0 +1,33 @@
<?php
error_reporting(E_ALL);
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
require __DIR__ . '/../src/phpFITFileAnalysis.php';
}
class IsPausedTest extends PHPUnit_Framework_TestCase
{
private $base_dir;
private $filename = 'power-analysis.fit';
private $pFFA;
public function setUp()
{
$this->base_dir = __DIR__ . '/../demo/fit_files/';
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
}
public function testIsPaused()
{
// isPaused() returns array of booleans using timestamp as key.
$is_paused = $this->pFFA->isPaused();
// Assert number of timestamps
$this->assertEquals(3190, count($is_paused));
// Assert an arbitrary element/timestamps is true
$this->assertEquals(true, $is_paused[1437477706]);
// Assert an arbitrary element/timestamps is false
$this->assertEquals(false, $is_paused[1437474517]);
}
}

@ -0,0 +1,157 @@
<?php
error_reporting(E_ALL);
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
require __DIR__ . '/../src/phpFITFileAnalysis.php';
}
class PowerTest extends PHPUnit_Framework_TestCase
{
private $base_dir;
private $filename = 'power-analysis.fit';
private $pFFA;
public function setUp()
{
$this->base_dir = __DIR__ . '/../demo/fit_files/';
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
}
public function testPower_criticalPower_values()
{
$time_periods = [2,5,10,60,300,600,1200,1800,3600];
$cps = $this->pFFA->criticalPower($time_periods);
array_walk($cps, function(&$v) { $v = round($v, 2); });
$this->assertEquals(551.50, $cps[2]);
$this->assertEquals(542.20, $cps[5]);
$this->assertEquals(527.70, $cps[10]);
$this->assertEquals(452.87, $cps[60]);
$this->assertEquals(361.99, $cps[300]);
$this->assertEquals(328.86, $cps[600]);
$this->assertEquals(260.52, $cps[1200]);
$this->assertEquals(221.81, $cps[1800]);
}
public function testPower_criticalPower_time_period_max()
{
// 14400 seconds is 4 hours and longer than file duration so should only get one result back (for 2 seconds)
$time_periods = [2,14400];
$cps = $this->pFFA->criticalPower($time_periods);
$this->assertEquals(1, count($cps));
}
public function testPower_powerMetrics()
{
$power_metrics = $this->pFFA->powerMetrics(350);
$this->assertEquals(221, $power_metrics['Average Power']);
$this->assertEquals(671, $power_metrics['Kilojoules']);
$this->assertEquals(285, $power_metrics['Normalised Power']);
$this->assertEquals(1.29, $power_metrics['Variability Index']);
$this->assertEquals(0.81, $power_metrics['Intensity Factor']);
$this->assertEquals(56, $power_metrics['Training Stress Score']);
}
public function testPower_power_partitioned()
{
// Calls phpFITFileAnalysis::powerZones();
$power_partioned = $this->pFFA->powerPartioned(350);
$this->assertEquals(45.2, $power_partioned['0-193']);
$this->assertEquals(10.8, $power_partioned['194-263']);
$this->assertEquals(18.1, $power_partioned['264-315']);
$this->assertEquals(17.9, $power_partioned['316-368']);
$this->assertEquals(4.2, $power_partioned['369-420']);
$this->assertEquals(3.3, $power_partioned['421-525']);
$this->assertEquals(0.4, $power_partioned['526+']);
}
public function testPower_powerHistogram()
{
// Calls phpFITFileAnalysis::histogram();
$power_histogram = $this->pFFA->powerHistogram(100);
$this->assertEquals(374, $power_histogram[0]);
$this->assertEquals(634, $power_histogram[100]);
$this->assertEquals(561, $power_histogram[200]);
$this->assertEquals(1103, $power_histogram[300]);
$this->assertEquals(301, $power_histogram[400]);
$this->assertEquals(66, $power_histogram[500]);
$this->assertEquals(4, $power_histogram[600]);
}
/**
* @expectedException Exception
*/
public function testPower_criticalPower_no_power()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit');
$time_periods = [2,14400];
$cps = $pFFA->criticalPower($time_periods);
}
/**
* @expectedException Exception
*/
public function testPower_powerMetrics_no_power()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit');
$power_metrics = $pFFA->powerMetrics(350);
}
/**
* @expectedException Exception
*/
public function testPower_powerHistogram_no_power()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit');
$power_metrics = $pFFA->powerHistogram(100);
}
/**
* @expectedException Exception
*/
public function testPower_powerHistogram_invalid_bucket_width()
{
$power_histogram = $this->pFFA->powerHistogram('INVALID');
}
/**
* @expectedException Exception
*/
public function testPower_power_partitioned_no_power()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit');
$power_partioned = $pFFA->powerPartioned(350);
}
/**
* @expectedException Exception
*/
public function testPower_power_partitioned_not_array()
{
$power_histogram = $this->pFFA->partitionData('power', 123456);
}
/**
* @expectedException Exception
*/
public function testPower_power_partitioned_not_numeric()
{
$power_histogram = $this->pFFA->partitionData('power', [200, 400, 'INVALID']);
}
/**
* @expectedException Exception
*/
public function testPower_power_partitioned_not_ascending()
{
$power_histogram = $this->pFFA->partitionData('power', [400, 200]);
}
}

@ -0,0 +1,51 @@
<?php
error_reporting(E_ALL);
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
require __DIR__ . '/../src/phpFITFileAnalysis.php';
}
class QuadrantAnalysisTest extends PHPUnit_Framework_TestCase
{
private $base_dir;
private $filename = 'power-analysis.fit';
private $pFFA;
public function setUp()
{
$this->base_dir = __DIR__ . '/../demo/fit_files/';
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
}
public function testQuadrantAnalysis()
{
$crank_length = 0.175;
$ftp = 329;
$selected_cadence = 90;
$use_timestamps = false;
// quadrantAnalysis() returns an array that can be used to plot CPV vs AEPF.
$quadrant_plot = $this->pFFA->quadrantAnalysis($crank_length, $ftp, $selected_cadence, $use_timestamps);
$this->assertEquals(90, $quadrant_plot['selected_cadence']);
$this->assertEquals(199.474, $quadrant_plot['aepf_threshold']);
$this->assertEquals(1.649, $quadrant_plot['cpv_threshold']);
$this->assertEquals(10.48, $quadrant_plot['quad_percent']['hf_hv']);
$this->assertEquals(10.61, $quadrant_plot['quad_percent']['hf_lv']);
$this->assertEquals(14.00, $quadrant_plot['quad_percent']['lf_hv']);
$this->assertEquals(64.91, $quadrant_plot['quad_percent']['lf_lv']);
$this->assertEquals(1.118, $quadrant_plot['plot'][0][0]);
$this->assertEquals(47.411, $quadrant_plot['plot'][0][1]);
$this->assertEquals(0.367, $quadrant_plot['ftp-25w'][0][0]);
$this->assertEquals(829.425, $quadrant_plot['ftp-25w'][0][1]);
$this->assertEquals(0.367, $quadrant_plot['ftp'][0][0]);
$this->assertEquals(897.634, $quadrant_plot['ftp'][0][1]);
$this->assertEquals(0.367, $quadrant_plot['ftp+25w'][0][0]);
$this->assertEquals(965.843, $quadrant_plot['ftp+25w'][0][1]);
}
}

@ -0,0 +1,60 @@
<?php
error_reporting(E_ALL);
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
require __DIR__ . '/../src/phpFITFileAnalysis.php';
}
class SetUnitsTest extends PHPUnit_Framework_TestCase
{
private $base_dir;
private $filename = 'road-cycling.fit';
public function setUp()
{
$this->base_dir = __DIR__ . '/../demo/fit_files/';
}
public function testSetUnits_validate_options_pass()
{
$valid_options = ['raw', 'statute', 'metric'];
foreach($valid_options as $valid_option) {
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => $valid_option]);
if($valid_option === 'raw') {
$this->assertEquals(1.286, reset($pFFA->data_mesgs['record']['speed']));
}
if($valid_option === 'statute') {
$this->assertEquals(2.877, reset($pFFA->data_mesgs['record']['speed']));
}
if($valid_option === 'metric') {
$this->assertEquals(4.63, reset($pFFA->data_mesgs['record']['speed']));
}
}
}
/**
* @expectedException Exception
*/
public function testSetUnits_validate_options_fail()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'INVALID']);
}
public function testSetUnits_validate_pace_option_pass()
{
$valid_options = [true, false];
foreach($valid_options as $valid_option) {
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw', 'pace' => $valid_option]);
$this->assertEquals(1.286, reset($pFFA->data_mesgs['record']['speed']));
}
}
/**
* @expectedException Exception
*/
public function testSetUnits_validate_pace_option_fail()
{
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['pace' => 'INVALID']);
}
}

@ -0,0 +1,7 @@
language: php
php:
- 5.3
- 5.4
- 5.5
script: phpunit --coverage-text ./

@ -0,0 +1,270 @@
<?php
class AltoRouter {
protected $routes = array();
protected $namedRoutes = array();
protected $basePath = '';
protected $matchTypes = array(
'i' => '[0-9]++',
'a' => '[0-9A-Za-z]++',
'h' => '[0-9A-Fa-f]++',
'*' => '.+?',
'**' => '.++',
'' => '[^/\.]++'
);
/**
* Create router in one call from config.
*
* @param array $routes
* @param string $basePath
* @param array $matchTypes
*/
public function __construct( $routes = array(), $basePath = '', $matchTypes = array() ) {
$this->addRoutes($routes);
$this->setBasePath($basePath);
$this->addMatchTypes($matchTypes);
}
/**
* Add multiple routes at once from array in the following format:
*
* $routes = array(
* array($method, $route, $target, $name)
* );
*
* @param array $routes
* @return void
* @author Koen Punt
*/
public function addRoutes($routes){
if(!is_array($routes) && !$routes instanceof Traversable) {
throw new \Exception('Routes should be an array or an instance of Traversable');
}
foreach($routes as $route) {
call_user_func_array(array($this, 'map'), $route);
}
}
/**
* Set the base path.
* Useful if you are running your application from a subdirectory.
*/
public function setBasePath($basePath) {
$this->basePath = $basePath;
}
/**
* Add named match types. It uses array_merge so keys can be overwritten.
*
* @param array $matchTypes The key is the name and the value is the regex.
*/
public function addMatchTypes($matchTypes) {
$this->matchTypes = array_merge($this->matchTypes, $matchTypes);
}
/**
* Map a route to a target
*
* @param string $method One of 4 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PUT|DELETE)
* @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id]
* @param mixed $target The target where this route should point to. Can be anything.
* @param string $name Optional name of this route. Supply if you want to reverse route this url in your application.
*/
public function map($method, $route, $target, $name = null) {
$this->routes[] = array($method, $route, $target, $name);
if($name) {
if(isset($this->namedRoutes[$name])) {
throw new \Exception("Can not redeclare route '{$name}'");
} else {
$this->namedRoutes[$name] = $route;
}
}
return;
}
/**
* Reversed routing
*
* Generate the URL for a named route. Replace regexes with supplied parameters
*
* @param string $routeName The name of the route.
* @param array @params Associative array of parameters to replace placeholders with.
* @return string The URL of the route with named parameters in place.
*/
public function generate($routeName, array $params = array()) {
// Check if named route exists
if(!isset($this->namedRoutes[$routeName])) {
throw new \Exception("Route '{$routeName}' does not exist.");
}
// Replace named parameters
$route = $this->namedRoutes[$routeName];
// prepend base path to route url again
$url = $this->basePath . $route;
if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {
foreach($matches as $match) {
list($block, $pre, $type, $param, $optional) = $match;
if ($pre) {
$block = substr($block, 1);
}
if(isset($params[$param])) {
$url = str_replace($block, $params[$param], $url);
} elseif ($optional) {
$url = str_replace($pre . $block, '', $url);
}
}
}
return $url;
}
/**
* Match a given Request Url against stored routes
* @param string $requestUrl
* @param string $requestMethod
* @return array|boolean Array with route information on success, false on failure (no match).
*/
public function match($requestUrl = null, $requestMethod = null) {
$params = array();
$match = false;
// set Request Url if it isn't passed as parameter
if($requestUrl === null) {
$requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
}
// strip base path from request url
$requestUrl = substr($requestUrl, strlen($this->basePath));
// Strip query string (?a=b) from Request Url
if (($strpos = strpos($requestUrl, '?')) !== false) {
$requestUrl = substr($requestUrl, 0, $strpos);
}
// set Request Method if it isn't passed as a parameter
if($requestMethod === null) {
$requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
}
// Force request_order to be GP
// http://www.mail-archive.com/internals@lists.php.net/msg33119.html
$_REQUEST = array_merge($_GET, $_POST);
foreach($this->routes as $handler) {
list($method, $_route, $target, $name) = $handler;
$methods = explode('|', $method);
$method_match = false;
// Check if request method matches. If not, abandon early. (CHEAP)
foreach($methods as $method) {
if (strcasecmp($requestMethod, $method) === 0) {
$method_match = true;
break;
}
}
// Method did not match, continue to next route.
if(!$method_match) continue;
// Check for a wildcard (matches all)
if ($_route === '*') {
$match = true;
} elseif (isset($_route[0]) && $_route[0] === '@') {
$match = preg_match('`' . substr($_route, 1) . '`u', $requestUrl, $params);
} else {
$route = null;
$regex = false;
$j = 0;
$n = isset($_route[0]) ? $_route[0] : null;
$i = 0;
// Find the longest non-regex substring and match it against the URI
while (true) {
if (!isset($_route[$i])) {
break;
} elseif (false === $regex) {
$c = $n;
$regex = $c === '[' || $c === '(' || $c === '.';
if (false === $regex && false !== isset($_route[$i+1])) {
$n = $_route[$i + 1];
$regex = $n === '?' || $n === '+' || $n === '*' || $n === '{';
}
if (false === $regex && $c !== '/' && (!isset($requestUrl[$j]) || $c !== $requestUrl[$j])) {
continue 2;
}
$j++;
}
$route .= $_route[$i++];
}
$regex = $this->compileRoute($route);
$match = preg_match($regex, $requestUrl, $params);
}
if(($match == true || $match > 0)) {
if($params) {
foreach($params as $key => $value) {
if(is_numeric($key)) unset($params[$key]);
}
}
return array(
'target' => $target,
'params' => $params,
'name' => $name
);
}
}
return false;
}
/**
* Compile the regex for a given route (EXPENSIVE)
*/
private function compileRoute($route) {
if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {
$matchTypes = $this->matchTypes;
foreach($matches as $match) {
list($block, $pre, $type, $param, $optional) = $match;
if (isset($matchTypes[$type])) {
$type = $matchTypes[$type];
}
if ($pre === '.') {
$pre = '\.';
}
//Older versions of PCRE require the 'P' in (?P<named>)
$pattern = '(?:'
. ($pre !== '' ? $pre : null)
. '('
. ($param !== '' ? "?P<$param>" : null)
. $type
. '))'
. ($optional !== '' ? '?' : null);
$route = str_replace($block, $pattern, $route);
}
}
return "`^$route$`u";
}
}

@ -0,0 +1,423 @@
<?php
require 'AltoRouter.php';
class AltoRouterDebug extends AltoRouter{
public function getNamedRoutes(){
return $this->namedRoutes;
}
public function getRoutes(){
return $this->routes;
}
public function getBasePath(){
return $this->basePath;
}
}
class SimpleTraversable implements Iterator{
protected $_position = 0;
protected $_data = array(
array('GET', '/foo', 'foo_action', null),
array('POST', '/bar', 'bar_action', 'second_route')
);
public function current(){
return $this->_data[$this->_position];
}
public function key(){
return $this->_position;
}
public function next(){
++$this->_position;
}
public function rewind(){
$this->_position = 0;
}
public function valid(){
return isset($this->_data[$this->_position]);
}
}
/**
* Generated by PHPUnit_SkeletonGenerator 1.2.1 on 2013-07-14 at 17:47:46.
*/
class AltoRouterTest extends PHPUnit_Framework_TestCase
{
/**
* @var AltoRouter
*/
protected $router;
/**
* Sets up the fixture, for example, opens a network connection.
* This method is called before a test is executed.
*/
protected function setUp()
{
$this->router = new AltoRouterDebug;
}
/**
* Tears down the fixture, for example, closes a network connection.
* This method is called after a test is executed.
*/
protected function tearDown()
{
}
/**
* @covers AltoRouter::addRoutes
*/
public function testAddRoutes()
{
$method = 'POST';
$route = '/[:controller]/[:action]';
$target = function(){};
$this->router->addRoutes(array(
array($method, $route, $target),
array($method, $route, $target, 'second_route')
));
$routes = $this->router->getRoutes();
$this->assertEquals(array($method, $route, $target, null), $routes[0]);
$this->assertEquals(array($method, $route, $target, 'second_route'), $routes[1]);
}
/**
* @covers AltoRouter::addRoutes
*/
public function testAddRoutesAcceptsTraverable()
{
$traversable = new SimpleTraversable();
$this->router->addRoutes($traversable);
$traversable->rewind();
$first = $traversable->current();
$traversable->next();
$second = $traversable->current();
$routes = $this->router->getRoutes();
$this->assertEquals($first, $routes[0]);
$this->assertEquals($second, $routes[1]);
}
/**
* @covers AltoRouter::addRoutes
* @expectedException Exception
*/
public function testAddRoutesThrowsExceptionOnInvalidArgument()
{
$this->router->addRoutes(new stdClass);
}
/**
* @covers AltoRouter::setBasePath
*/
public function testSetBasePath()
{
$basePath = $this->router->setBasePath('/some/path');
$this->assertEquals('/some/path', $this->router->getBasePath());
$basePath = $this->router->setBasePath('/some/path');
$this->assertEquals('/some/path', $this->router->getBasePath());
}
/**
* @covers AltoRouter::map
*/
public function testMap()
{
$method = 'POST';
$route = '/[:controller]/[:action]';
$target = function(){};
$this->router->map($method, $route, $target);
$routes = $this->router->getRoutes();
$this->assertEquals(array($method, $route, $target, null), $routes[0]);
}
/**
* @covers AltoRouter::map
*/
public function testMapWithName()
{
$method = 'POST';
$route = '/[:controller]/[:action]';
$target = function(){};
$name = 'myroute';
$this->router->map($method, $route, $target, $name);
$routes = $this->router->getRoutes();
$this->assertEquals(array($method, $route, $target, $name), $routes[0]);
$named_routes = $this->router->getNamedRoutes();
$this->assertEquals($route, $named_routes[$name]);
try{
$this->router->map($method, $route, $target, $name);
$this->fail('Should not be able to add existing named route');
}catch(Exception $e){
$this->assertEquals("Can not redeclare route '{$name}'", $e->getMessage());
}
}
/**
* @covers AltoRouter::generate
*/
public function testGenerate()
{
$params = array(
'controller' => 'test',
'action' => 'someaction'
);
$this->router->map('GET', '/[:controller]/[:action]', function(){}, 'foo_route');
$this->assertEquals('/test/someaction',
$this->router->generate('foo_route', $params));
$params = array(
'controller' => 'test',
'action' => 'someaction',
'type' => 'json'
);
$this->assertEquals('/test/someaction',
$this->router->generate('foo_route', $params));
}
public function testGenerateWithOptionalUrlParts()
{
$this->router->map('GET', '/[:controller]/[:action].[:type]?', function(){}, 'bar_route');
$params = array(
'controller' => 'test',
'action' => 'someaction'
);
$this->assertEquals('/test/someaction',
$this->router->generate('bar_route', $params));
$params = array(
'controller' => 'test',
'action' => 'someaction',
'type' => 'json'
);
$this->assertEquals('/test/someaction.json',
$this->router->generate('bar_route', $params));
}
public function testGenerateWithNonexistingRoute()
{
try{
$this->router->generate('nonexisting_route');
$this->fail('Should trigger an exception on nonexisting named route');
}catch(Exception $e){
$this->assertEquals("Route 'nonexisting_route' does not exist.", $e->getMessage());
}
}
/**
* @covers AltoRouter::match
* @covers AltoRouter::compileRoute
*/
public function testMatch()
{
$this->router->map('GET', '/foo/[:controller]/[:action]', 'foo_action', 'foo_route');
$this->assertEquals(array(
'target' => 'foo_action',
'params' => array(
'controller' => 'test',
'action' => 'do'
),
'name' => 'foo_route'
), $this->router->match('/foo/test/do', 'GET'));
$this->assertFalse($this->router->match('/foo/test/do', 'POST'));
$this->assertEquals(array(
'target' => 'foo_action',
'params' => array(
'controller' => 'test',
'action' => 'do'
),
'name' => 'foo_route'
), $this->router->match('/foo/test/do?param=value', 'GET'));
}
public function testMatchWithFixedParamValues()
{
$this->router->map('POST','/users/[i:id]/[delete|update:action]', 'usersController#doAction', 'users_do');
$this->assertEquals(array(
'target' => 'usersController#doAction',
'params' => array(
'id' => 1,
'action' => 'delete'
),
'name' => 'users_do'
), $this->router->match('/users/1/delete', 'POST'));
$this->assertFalse($this->router->match('/users/1/delete', 'GET'));
$this->assertFalse($this->router->match('/users/abc/delete', 'POST'));
$this->assertFalse($this->router->match('/users/1/create', 'GET'));
}
public function testMatchWithServerVars()
{
$this->router->map('GET', '/foo/[:controller]/[:action]', 'foo_action', 'foo_route');
$_SERVER['REQUEST_URI'] = '/foo/test/do';
$_SERVER['REQUEST_METHOD'] = 'GET';
$this->assertEquals(array(
'target' => 'foo_action',
'params' => array(
'controller' => 'test',
'action' => 'do'
),
'name' => 'foo_route'
), $this->router->match());
}
public function testMatchWithOptionalUrlParts()
{
$this->router->map('GET', '/bar/[:controller]/[:action].[:type]?', 'bar_action', 'bar_route');
$this->assertEquals(array(
'target' => 'bar_action',
'params' => array(
'controller' => 'test',
'action' => 'do',
'type' => 'json'
),
'name' => 'bar_route'
), $this->router->match('/bar/test/do.json', 'GET'));
}
public function testMatchWithWildcard()
{
$this->router->map('GET', '/a', 'foo_action', 'foo_route');
$this->router->map('GET', '*', 'bar_action', 'bar_route');
$this->assertEquals(array(
'target' => 'bar_action',
'params' => array(),
'name' => 'bar_route'
), $this->router->match('/everything', 'GET'));
}
public function testMatchWithCustomRegexp()
{
$this->router->map('GET', '@^/[a-z]*$', 'bar_action', 'bar_route');
$this->assertEquals(array(
'target' => 'bar_action',
'params' => array(),
'name' => 'bar_route'
), $this->router->match('/everything', 'GET'));
$this->assertFalse($this->router->match('/some-other-thing', 'GET'));
}
public function testMatchWithUnicodeRegex()
{
$pattern = '/(?<path>[^';
// Arabic characters
$pattern .= '\x{0600}-\x{06FF}';
$pattern .= '\x{FB50}-\x{FDFD}';
$pattern .= '\x{FE70}-\x{FEFF}';
$pattern .= '\x{0750}-\x{077F}';
// Alphanumeric, /, _, - and space characters
$pattern .= 'a-zA-Z0-9\/_-\s';
// 'ZERO WIDTH NON-JOINER'
$pattern .= '\x{200C}';
$pattern .= ']+)';
$this->router->map('GET', '@' . $pattern, 'unicode_action', 'unicode_route');
$this->assertEquals(array(
'target' => 'unicode_action',
'name' => 'unicode_route',
'params' => array(
'path' => '大家好'
)
), $this->router->match('/大家好', 'GET'));
$this->assertFalse($this->router->match('/﷽‎', 'GET'));
}
/**
* @covers AltoRouter::addMatchTypes
*/
public function testMatchWithCustomNamedRegex()
{
$this->router->addMatchTypes(array('cId' => '[a-zA-Z]{2}[0-9](?:_[0-9]++)?'));
$this->router->map('GET', '/bar/[cId:customId]', 'bar_action', 'bar_route');
$this->assertEquals(array(
'target' => 'bar_action',
'params' => array(
'customId' => 'AB1',
),
'name' => 'bar_route'
), $this->router->match('/bar/AB1', 'GET'));
$this->assertEquals(array(
'target' => 'bar_action',
'params' => array(
'customId' => 'AB1_0123456789',
),
'name' => 'bar_route'
), $this->router->match('/bar/AB1_0123456789', 'GET'));
$this->assertFalse($this->router->match('/some-other-thing', 'GET'));
}
public function testMatchWithCustomNamedUnicodeRegex()
{
$pattern = '[^';
// Arabic characters
$pattern .= '\x{0600}-\x{06FF}';
$pattern .= '\x{FB50}-\x{FDFD}';
$pattern .= '\x{FE70}-\x{FEFF}';
$pattern .= '\x{0750}-\x{077F}';
$pattern .= ']+';
$this->router->addMatchTypes(array('nonArabic' => $pattern));
$this->router->map('GET', '/bar/[nonArabic:string]', 'non_arabic_action', 'non_arabic_route');
$this->assertEquals(array(
'target' => 'non_arabic_action',
'name' => 'non_arabic_route',
'params' => array(
'string' => 'some-path'
)
), $this->router->match('/bar/some-path', 'GET'));
$this->assertFalse($this->router->match('/﷽‎', 'GET'));
}
}

@ -0,0 +1,92 @@
# AltoRouter [![Build Status](https://api.travis-ci.org/dannyvankooten/AltoRouter.png)](http://travis-ci.org/dannyvankooten/AltoRouter)
AltoRouter is a small but powerful routing class for PHP 5.3+, heavily inspired by [klein.php](https://github.com/chriso/klein.php/).
* Dynamic routing with named parameters
* Reversed routing
* Flexible regular expression routing (inspired by [Sinatra](http://www.sinatrarb.com/))
* Custom regexes
## Getting started
1. PHP 5.3.x is required
2. Install AltoRouter using Composer or manually
2. Setup URL rewriting so that all requests are handled by **index.php**
3. Create an instance of AltoRouter, map your routes and match a request.
4. Have a look at the basic example in the `examples` directory for a better understanding on how to use AltoRouter.
## Routing
```php
$router = new AltoRouter();
$router->setBasePath('/AltoRouter'); // (optional) the subdir AltoRouter lives in
// mapping routes
$router->map('GET|POST','/', 'home#index', 'home');
$router->map('GET','/users', array('c' => 'UserController', 'a' => 'ListAction'));
$router->map('GET','/users/[i:id]', 'users#show', 'users_show');
$router->map('POST','/users/[i:id]/[delete|update:action]', 'usersController#doAction', 'users_do');
// reversed routing
$router->generate('users_show', array('id' => 5));
```
**You can use the following limits on your named parameters. AltoRouter will create the correct regexes for you.**
```php
* // Match all request URIs
[i] // Match an integer
[i:id] // Match an integer as 'id'
[a:action] // Match alphanumeric characters as 'action'
[h:key] // Match hexadecimal characters as 'key'
[:action] // Match anything up to the next / or end of the URI as 'action'
[create|edit:action] // Match either 'create' or 'edit' as 'action'
[*] // Catch all (lazy, stops at the next trailing slash)
[*:trailing] // Catch all as 'trailing' (lazy)
[**:trailing] // Catch all (possessive - will match the rest of the URI)
.[:format]? // Match an optional parameter 'format' - a / or . before the block is also optional
```
**Some more complicated examples**
```php
@/(?[A-Za-z]{2}_[A-Za-z]{2})$ // custom regex, matches language codes like "en_us" etc.
/posts/[*:title][i:id] // Matches "/posts/this-is-a-title-123"
/output.[xml|json:format]? // Matches "/output", "output.xml", "output.json"
/[:controller]?/[:action]? // Matches the typical /controller/action format
```
**The character before the colon (the 'match type') is a shortcut for one of the following regular expressions**
```php
'i' => '[0-9]++'
'a' => '[0-9A-Za-z]++'
'h' => '[0-9A-Fa-f]++'
'*' => '.+?'
'**' => '.++'
'' => '[^/\.]++'
```
**New match types can be added using the `addMatchTypes()` method**
```php
$router->addMatchTypes(array('cId' => '[a-zA-Z]{2}[0-9](?:_[0-9]++)?'));
```
## Contributors
- [Danny van Kooten](https://github.com/dannyvankooten)
- [Koen Punt](https://github.com/koenpunt)
- [John Long](https://github.com/adduc)
- [Niahoo Osef](https://github.com/niahoo)
## License
(MIT License)
Copyright (c) 2012-2013 Danny van Kooten <hi@dannyvankooten.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,28 @@
{
"name": "altorouter/altorouter",
"description": "A lightning fast router for PHP",
"keywords": ["router", "routing", "lightweight"],
"homepage": "https://github.com/dannyvankooten/AltoRouter",
"license": "MIT",
"authors": [
{
"name": "Danny van Kooten",
"email": "dannyvankooten@gmail.com",
"homepage": "http://dannyvankooten.com/"
},
{
"name": "Koen Punt",
"homepage": "https://github.com/koenpunt"
},
{
"name": "niahoo",
"homepage": "https://github.com/niahoo"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"classmap": ["AltoRouter.php"]
}
}

@ -0,0 +1,3 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . index.php [L]

@ -0,0 +1,27 @@
<?php
require '../../AltoRouter.php';
$router = new AltoRouter();
$router->setBasePath('/AltoRouter/examples/basic');
$router->map('GET|POST','/', 'home#index', 'home');
$router->map('GET','/users/', array('c' => 'UserController', 'a' => 'ListAction'));
$router->map('GET','/users/[i:id]', 'users#show', 'users_show');
$router->map('POST','/users/[i:id]/[delete|update:action]', 'usersController#doAction', 'users_do');
// match current request
$match = $router->match();
?>
<h1>AltoRouter</h1>
<h3>Current request: </h3>
<pre>
Target: <?php var_dump($match['target']); ?>
Params: <?php var_dump($match['params']); ?>
Name: <?php var_dump($match['name']); ?>
</pre>
<h3>Try these requests: </h3>
<p><a href="<?php echo $router->generate('home'); ?>">GET <?php echo $router->generate('home'); ?></a></p>
<p><a href="<?php echo $router->generate('users_show', array('id' => 5)); ?>">GET <?php echo $router->generate('users_show', array('id' => 5)); ?></a></p>
<p><form action="<?php echo $router->generate('users_do', array('id' => 10, 'action' => 'update')); ?>" method="post"><button type="submit"><?php echo $router->generate('users_do', array('id' => 10, 'action' => 'update')); ?></button></form></p>

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit1887e85fc3cfddacf8d7e17588dae6f1::getLoader();

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
}
}
return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';

@ -0,0 +1,122 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = 'phpvfscomposer://'.$this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
$data = str_replace('__FILE__', var_export($this->realpath, true), $data);
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
}
}
return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load Diff

@ -0,0 +1,14 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
);

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

@ -0,0 +1,39 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'adriangibbons\\' => array($vendorDir . '/adriangibbons/php-fit-file-analysis/src'),
'Twig\\' => array($vendorDir . '/twig/twig/src'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Stub\\' => array($baseDir . '/src/data/stub', $baseDir . '/src/data/stub/service', $baseDir . '/src/data/stub/repository'),
'Shared\\Exception\\' => array($baseDir . '/src/shared/exception'),
'Shared\\Attributes\\' => array($baseDir . '/src/shared/attributes'),
'Shared\\' => array($baseDir . '/src/shared'),
'Repository\\' => array($baseDir . '/src/data/model/repository'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'Network\\' => array($baseDir . '/src/data/core/network'),
'Model\\' => array($baseDir . '/src/data/model'),
'Manager\\' => array($baseDir . '/src/data/model/manager'),
'Hearttrack\\' => array($baseDir . '/src'),
'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'),
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
'Data\\Core\\' => array($baseDir . '/src/data/core'),
'Data\\' => array($baseDir . '/src/data'),
'Console\\' => array($baseDir . '/src/console'),
'App\\Views\\Directives\\' => array($baseDir . '/src/app/views/directives'),
'App\\Router\\Response\\' => array($baseDir . '/src/app/router/response'),
'App\\Router\\Request\\' => array($baseDir . '/src/app/router/request'),
'App\\Router\\Middleware\\' => array($baseDir . '/src/app/router/middleware'),
'App\\Router\\' => array($baseDir . '/src/app/router'),
'App\\Controller\\' => array($baseDir . '/src/app/controller'),
'App\\' => array($baseDir . '/src/app'),
);

@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit1887e85fc3cfddacf8d7e17588dae6f1
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit1887e85fc3cfddacf8d7e17588dae6f1', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit1887e85fc3cfddacf8d7e17588dae6f1', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit1887e85fc3cfddacf8d7e17588dae6f1::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit1887e85fc3cfddacf8d7e17588dae6f1::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,347 @@
<?php return array(
'root' => array(
'name' => 'hearttrack/package',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'eb625309ce4a814abafb1d0ded3d0d5937ddc905',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'adriangibbons/php-fit-file-analysis' => array(
'pretty_version' => 'v3.2.4',
'version' => '3.2.4.0',
'reference' => '8efd36b1b963f01c42dc5329626519c040dec664',
'type' => 'library',
'install_path' => __DIR__ . '/../adriangibbons/php-fit-file-analysis',
'aliases' => array(),
'dev_requirement' => false,
),
'altorouter/altorouter' => array(
'pretty_version' => 'v1.1.0',
'version' => '1.1.0.0',
'reference' => '09d9d946c546bae6d22a7654cdb3b825ffda54b4',
'type' => 'library',
'install_path' => __DIR__ . '/../altorouter/altorouter',
'aliases' => array(),
'dev_requirement' => false,
),
'graham-campbell/result-type' => array(
'pretty_version' => 'v1.1.2',
'version' => '1.1.2.0',
'reference' => 'fbd48bce38f73f8a4ec8583362e732e4095e5862',
'type' => 'library',
'install_path' => __DIR__ . '/../graham-campbell/result-type',
'aliases' => array(),
'dev_requirement' => false,
),
'hearttrack/package' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'eb625309ce4a814abafb1d0ded3d0d5937ddc905',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'myclabs/deep-copy' => array(
'pretty_version' => '1.11.1',
'version' => '1.11.1.0',
'reference' => '7284c22080590fb39f2ffa3e9057f10a4ddd0e0c',
'type' => 'library',
'install_path' => __DIR__ . '/../myclabs/deep-copy',
'aliases' => array(),
'dev_requirement' => true,
),
'nikic/php-parser' => array(
'pretty_version' => 'v4.17.1',
'version' => '4.17.1.0',
'reference' => 'a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d',
'type' => 'library',
'install_path' => __DIR__ . '/../nikic/php-parser',
'aliases' => array(),
'dev_requirement' => true,
),
'phar-io/manifest' => array(
'pretty_version' => '2.0.3',
'version' => '2.0.3.0',
'reference' => '97803eca37d319dfa7826cc2437fc020857acb53',
'type' => 'library',
'install_path' => __DIR__ . '/../phar-io/manifest',
'aliases' => array(),
'dev_requirement' => true,
),
'phar-io/version' => array(
'pretty_version' => '3.2.1',
'version' => '3.2.1.0',
'reference' => '4f7fd7836c6f332bb2933569e566a0d6c4cbed74',
'type' => 'library',
'install_path' => __DIR__ . '/../phar-io/version',
'aliases' => array(),
'dev_requirement' => true,
),
'phpoption/phpoption' => array(
'pretty_version' => '1.9.2',
'version' => '1.9.2.0',
'reference' => '80735db690fe4fc5c76dfa7f9b770634285fa820',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoption/phpoption',
'aliases' => array(),
'dev_requirement' => false,
),
'phpunit/php-code-coverage' => array(
'pretty_version' => '10.1.9',
'version' => '10.1.9.0',
'reference' => 'a56a9ab2f680246adcf3db43f38ddf1765774735',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-code-coverage',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/php-file-iterator' => array(
'pretty_version' => '4.1.0',
'version' => '4.1.0.0',
'reference' => 'a95037b6d9e608ba092da1b23931e537cadc3c3c',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-file-iterator',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/php-invoker' => array(
'pretty_version' => '4.0.0',
'version' => '4.0.0.0',
'reference' => 'f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-invoker',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/php-text-template' => array(
'pretty_version' => '3.0.1',
'version' => '3.0.1.0',
'reference' => '0c7b06ff49e3d5072f057eb1fa59258bf287a748',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-text-template',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/php-timer' => array(
'pretty_version' => '6.0.0',
'version' => '6.0.0.0',
'reference' => 'e2a2d67966e740530f4a3343fe2e030ffdc1161d',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-timer',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/phpunit' => array(
'pretty_version' => '10.4.2',
'version' => '10.4.2.0',
'reference' => 'cacd8b9dd224efa8eb28beb69004126c7ca1a1a1',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/phpunit',
'aliases' => array(),
'dev_requirement' => true,
),
'psr/container' => array(
'pretty_version' => '2.0.2',
'version' => '2.0.2.0',
'reference' => 'c71ecc56dfe541dbd90c5360474fbc405f8d5963',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/container',
'aliases' => array(),
'dev_requirement' => false,
),
'sebastian/cli-parser' => array(
'pretty_version' => '2.0.0',
'version' => '2.0.0.0',
'reference' => 'efdc130dbbbb8ef0b545a994fd811725c5282cae',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/cli-parser',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/code-unit' => array(
'pretty_version' => '2.0.0',
'version' => '2.0.0.0',
'reference' => 'a81fee9eef0b7a76af11d121767abc44c104e503',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/code-unit',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/code-unit-reverse-lookup' => array(
'pretty_version' => '3.0.0',
'version' => '3.0.0.0',
'reference' => '5e3a687f7d8ae33fb362c5c0743794bbb2420a1d',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/code-unit-reverse-lookup',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/comparator' => array(
'pretty_version' => '5.0.1',
'version' => '5.0.1.0',
'reference' => '2db5010a484d53ebf536087a70b4a5423c102372',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/comparator',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/complexity' => array(
'pretty_version' => '3.1.0',
'version' => '3.1.0.0',
'reference' => '68cfb347a44871f01e33ab0ef8215966432f6957',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/complexity',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/diff' => array(
'pretty_version' => '5.0.3',
'version' => '5.0.3.0',
'reference' => '912dc2fbe3e3c1e7873313cc801b100b6c68c87b',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/diff',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/environment' => array(
'pretty_version' => '6.0.1',
'version' => '6.0.1.0',
'reference' => '43c751b41d74f96cbbd4e07b7aec9675651e2951',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/environment',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/exporter' => array(
'pretty_version' => '5.1.1',
'version' => '5.1.1.0',
'reference' => '64f51654862e0f5e318db7e9dcc2292c63cdbddc',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/exporter',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/global-state' => array(
'pretty_version' => '6.0.1',
'version' => '6.0.1.0',
'reference' => '7ea9ead78f6d380d2a667864c132c2f7b83055e4',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/global-state',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/lines-of-code' => array(
'pretty_version' => '2.0.1',
'version' => '2.0.1.0',
'reference' => '649e40d279e243d985aa8fb6e74dd5bb28dc185d',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/lines-of-code',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/object-enumerator' => array(
'pretty_version' => '5.0.0',
'version' => '5.0.0.0',
'reference' => '202d0e344a580d7f7d04b3fafce6933e59dae906',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/object-enumerator',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/object-reflector' => array(
'pretty_version' => '3.0.0',
'version' => '3.0.0.0',
'reference' => '24ed13d98130f0e7122df55d06c5c4942a577957',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/object-reflector',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/recursion-context' => array(
'pretty_version' => '5.0.0',
'version' => '5.0.0.0',
'reference' => '05909fb5bc7df4c52992396d0116aed689f93712',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/recursion-context',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/type' => array(
'pretty_version' => '4.0.0',
'version' => '4.0.0.0',
'reference' => '462699a16464c3944eefc02ebdd77882bd3925bf',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/type',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/version' => array(
'pretty_version' => '4.0.1',
'version' => '4.0.1.0',
'reference' => 'c51fa83a5d8f43f1402e3f32a005e6262244ef17',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/version',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.28.0',
'version' => '1.28.0.0',
'reference' => 'ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.28.0',
'version' => '1.28.0.0',
'reference' => '42292d99c55abe617799667f454222c54c60e229',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.28.0',
'version' => '1.28.0.0',
'reference' => '6caa57379c4aec19c0a12a38b59b26487dcfe4b5',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'dev_requirement' => false,
),
'theseer/tokenizer' => array(
'pretty_version' => '1.2.2',
'version' => '1.2.2.0',
'reference' => 'b2ad5003ca10d4ee50a12da31de12a5774ba6b96',
'type' => 'library',
'install_path' => __DIR__ . '/../theseer/tokenizer',
'aliases' => array(),
'dev_requirement' => true,
),
'twig/twig' => array(
'pretty_version' => 'v3.8.0',
'version' => '3.8.0.0',
'reference' => '9d15f0ac07f44dc4217883ec6ae02fd555c6f71d',
'type' => 'library',
'install_path' => __DIR__ . '/../twig/twig',
'aliases' => array(),
'dev_requirement' => false,
),
'vlucas/phpdotenv' => array(
'pretty_version' => 'v5.6.0',
'version' => '5.6.0.0',
'reference' => '2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4',
'type' => 'library',
'install_path' => __DIR__ . '/../vlucas/phpdotenv',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70400)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020-2023 Graham Campbell <hello@gjcampbell.co.uk>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,33 @@
{
"name": "graham-campbell/result-type",
"description": "An Implementation Of The Result Type",
"keywords": ["result", "result-type", "Result", "Result Type", "Result-Type", "Graham Campbell", "GrahamCampbell"],
"license": "MIT",
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"GrahamCampbell\\Tests\\ResultType\\": "tests/"
}
},
"config": {
"preferred-install": "dist"
}
}

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
/*
* This file is part of Result Type.
*
* (c) Graham Campbell <hello@gjcampbell.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace GrahamCampbell\ResultType;
use PhpOption\None;
use PhpOption\Some;
/**
* @template T
* @template E
*
* @extends \GrahamCampbell\ResultType\Result<T,E>
*/
final class Error extends Result
{
/**
* @var E
*/
private $value;
/**
* Internal constructor for an error value.
*
* @param E $value
*
* @return void
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Create a new error value.
*
* @template F
*
* @param F $value
*
* @return \GrahamCampbell\ResultType\Result<T,F>
*/
public static function create($value)
{
return new self($value);
}
/**
* Get the success option value.
*
* @return \PhpOption\Option<T>
*/
public function success()
{
return None::create();
}
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \GrahamCampbell\ResultType\Result<S,E>
*/
public function map(callable $f)
{
return self::create($this->value);
}
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
*
* @return \GrahamCampbell\ResultType\Result<S,F>
*/
public function flatMap(callable $f)
{
/** @var \GrahamCampbell\ResultType\Result<S,F> */
return self::create($this->value);
}
/**
* Get the error option value.
*
* @return \PhpOption\Option<E>
*/
public function error()
{
return Some::create($this->value);
}
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \GrahamCampbell\ResultType\Result<T,F>
*/
public function mapError(callable $f)
{
return self::create($f($this->value));
}
}

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This file is part of Result Type.
*
* (c) Graham Campbell <hello@gjcampbell.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace GrahamCampbell\ResultType;
/**
* @template T
* @template E
*/
abstract class Result
{
/**
* Get the success option value.
*
* @return \PhpOption\Option<T>
*/
abstract public function success();
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \GrahamCampbell\ResultType\Result<S,E>
*/
abstract public function map(callable $f);
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
*
* @return \GrahamCampbell\ResultType\Result<S,F>
*/
abstract public function flatMap(callable $f);
/**
* Get the error option value.
*
* @return \PhpOption\Option<E>
*/
abstract public function error();
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \GrahamCampbell\ResultType\Result<T,F>
*/
abstract public function mapError(callable $f);
}

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
/*
* This file is part of Result Type.
*
* (c) Graham Campbell <hello@gjcampbell.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace GrahamCampbell\ResultType;
use PhpOption\None;
use PhpOption\Some;
/**
* @template T
* @template E
*
* @extends \GrahamCampbell\ResultType\Result<T,E>
*/
final class Success extends Result
{
/**
* @var T
*/
private $value;
/**
* Internal constructor for a success value.
*
* @param T $value
*
* @return void
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Create a new error value.
*
* @template S
*
* @param S $value
*
* @return \GrahamCampbell\ResultType\Result<S,E>
*/
public static function create($value)
{
return new self($value);
}
/**
* Get the success option value.
*
* @return \PhpOption\Option<T>
*/
public function success()
{
return Some::create($this->value);
}
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \GrahamCampbell\ResultType\Result<S,E>
*/
public function map(callable $f)
{
return self::create($f($this->value));
}
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
*
* @return \GrahamCampbell\ResultType\Result<S,F>
*/
public function flatMap(callable $f)
{
return $f($this->value);
}
/**
* Get the error option value.
*
* @return \PhpOption\Option<E>
*/
public function error()
{
return None::create();
}
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \GrahamCampbell\ResultType\Result<T,F>
*/
public function mapError(callable $f)
{
return self::create($this->value);
}
}

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: "packagist/myclabs/deep-copy"
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

@ -0,0 +1,101 @@
name: "Continuous Integration"
on:
- pull_request
- push
env:
COMPOSER_ROOT_VERSION: 1.99
jobs:
composer-json-lint:
name: "Lint composer.json"
runs-on: "ubuntu-latest"
strategy:
matrix:
php-version:
- "8.1"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
tools: composer-normalize
- name: "Get composer cache directory"
id: composercache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Cache dependencies"
uses: actions/cache@v2
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ matrix.dependencies }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ matrix.dependencies }}-composer-
- name: "Install dependencies"
run: "composer update --no-interaction --no-progress"
- name: "Validate composer.json"
run: "composer validate --strict"
- name: "Normalize composer.json"
run: "composer-normalize --dry-run"
tests:
name: "Tests"
runs-on: "ubuntu-latest"
strategy:
matrix:
php-version:
- "7.1"
- "7.2"
- "7.3"
- "7.4"
- "8.0"
- "8.1"
dependencies:
- "lowest"
- "highest"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-version }}"
ini-values: zend.assertions=1
- name: "Get composer cache directory"
id: composercache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Cache dependencies"
uses: actions/cache@v2
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ matrix.dependencies }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ matrix.dependencies }}-composer-
- name: "Install lowest dependencies"
if: ${{ matrix.dependencies == 'lowest' }}
run: "composer update --no-interaction --no-progress --prefer-lowest"
- name: "Install highest dependencies"
if: ${{ matrix.dependencies == 'highest' }}
run: "composer update --no-interaction --no-progress"
- name: "Run tests"
timeout-minutes: 3
run: "vendor/bin/phpunit"

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 My C-Sense
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,406 @@
# DeepCopy
DeepCopy helps you create deep copies (clones) of your objects. It is designed to handle cycles in the association graph.
[![Total Downloads](https://poser.pugx.org/myclabs/deep-copy/downloads.svg)](https://packagist.org/packages/myclabs/deep-copy)
[![Integrate](https://github.com/myclabs/DeepCopy/workflows/ci/badge.svg?branch=1.x)](https://github.com/myclabs/DeepCopy/actions)
## Table of Contents
1. [How](#how)
1. [Why](#why)
1. [Using simply `clone`](#using-simply-clone)
1. [Overriding `__clone()`](#overriding-__clone)
1. [With `DeepCopy`](#with-deepcopy)
1. [How it works](#how-it-works)
1. [Going further](#going-further)
1. [Matchers](#matchers)
1. [Property name](#property-name)
1. [Specific property](#specific-property)
1. [Type](#type)
1. [Filters](#filters)
1. [`SetNullFilter`](#setnullfilter-filter)
1. [`KeepFilter`](#keepfilter-filter)
1. [`DoctrineCollectionFilter`](#doctrinecollectionfilter-filter)
1. [`DoctrineEmptyCollectionFilter`](#doctrineemptycollectionfilter-filter)
1. [`DoctrineProxyFilter`](#doctrineproxyfilter-filter)
1. [`ReplaceFilter`](#replacefilter-type-filter)
1. [`ShallowCopyFilter`](#shallowcopyfilter-type-filter)
1. [Edge cases](#edge-cases)
1. [Contributing](#contributing)
1. [Tests](#tests)
## How?
Install with Composer:
```
composer require myclabs/deep-copy
```
Use it:
```php
use DeepCopy\DeepCopy;
$copier = new DeepCopy();
$myCopy = $copier->copy($myObject);
```
## Why?
- How do you create copies of your objects?
```php
$myCopy = clone $myObject;
```
- How do you create **deep** copies of your objects (i.e. copying also all the objects referenced in the properties)?
You use [`__clone()`](http://www.php.net/manual/en/language.oop5.cloning.php#object.clone) and implement the behavior
yourself.
- But how do you handle **cycles** in the association graph?
Now you're in for a big mess :(
![association graph](doc/graph.png)
### Using simply `clone`
![Using clone](doc/clone.png)
### Overriding `__clone()`
![Overriding __clone](doc/deep-clone.png)
### With `DeepCopy`
![With DeepCopy](doc/deep-copy.png)
## How it works
DeepCopy recursively traverses all the object's properties and clones them. To avoid cloning the same object twice it
keeps a hash map of all instances and thus preserves the object graph.
To use it:
```php
use function DeepCopy\deep_copy;
$copy = deep_copy($var);
```
Alternatively, you can create your own `DeepCopy` instance to configure it differently for example:
```php
use DeepCopy\DeepCopy;
$copier = new DeepCopy(true);
$copy = $copier->copy($var);
```
You may want to roll your own deep copy function:
```php
namespace Acme;
use DeepCopy\DeepCopy;
function deep_copy($var)
{
static $copier = null;
if (null === $copier) {
$copier = new DeepCopy(true);
}
return $copier->copy($var);
}
```
## Going further
You can add filters to customize the copy process.
The method to add a filter is `DeepCopy\DeepCopy::addFilter($filter, $matcher)`,
with `$filter` implementing `DeepCopy\Filter\Filter`
and `$matcher` implementing `DeepCopy\Matcher\Matcher`.
We provide some generic filters and matchers.
### Matchers
- `DeepCopy\Matcher` applies on a object attribute.
- `DeepCopy\TypeMatcher` applies on any element found in graph, including array elements.
#### Property name
The `PropertyNameMatcher` will match a property by its name:
```php
use DeepCopy\Matcher\PropertyNameMatcher;
// Will apply a filter to any property of any objects named "id"
$matcher = new PropertyNameMatcher('id');
```
#### Specific property
The `PropertyMatcher` will match a specific property of a specific class:
```php
use DeepCopy\Matcher\PropertyMatcher;
// Will apply a filter to the property "id" of any objects of the class "MyClass"
$matcher = new PropertyMatcher('MyClass', 'id');
```
#### Type
The `TypeMatcher` will match any element by its type (instance of a class or any value that could be parameter of
[gettype()](http://php.net/manual/en/function.gettype.php) function):
```php
use DeepCopy\TypeMatcher\TypeMatcher;
// Will apply a filter to any object that is an instance of Doctrine\Common\Collections\Collection
$matcher = new TypeMatcher('Doctrine\Common\Collections\Collection');
```
### Filters
- `DeepCopy\Filter` applies a transformation to the object attribute matched by `DeepCopy\Matcher`
- `DeepCopy\TypeFilter` applies a transformation to any element matched by `DeepCopy\TypeMatcher`
By design, matching a filter will stop the chain of filters (i.e. the next ones will not be applied).
Using the ([`ChainableFilter`](#chainablefilter-filter)) won't stop the chain of filters.
#### `SetNullFilter` (filter)
Let's say for example that you are copying a database record (or a Doctrine entity), so you want the copy not to have
any ID:
```php
use DeepCopy\DeepCopy;
use DeepCopy\Filter\SetNullFilter;
use DeepCopy\Matcher\PropertyNameMatcher;
$object = MyClass::load(123);
echo $object->id; // 123
$copier = new DeepCopy();
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));
$copy = $copier->copy($object);
echo $copy->id; // null
```
#### `KeepFilter` (filter)
If you want a property to remain untouched (for example, an association to an object):
```php
use DeepCopy\DeepCopy;
use DeepCopy\Filter\KeepFilter;
use DeepCopy\Matcher\PropertyMatcher;
$copier = new DeepCopy();
$copier->addFilter(new KeepFilter(), new PropertyMatcher('MyClass', 'category'));
$copy = $copier->copy($object);
// $copy->category has not been touched
```
#### `ChainableFilter` (filter)
If you use cloning on proxy classes, you might want to apply two filters for:
1. loading the data
2. applying a transformation
You can use the `ChainableFilter` as a decorator of the proxy loader filter, which won't stop the chain of filters (i.e.
the next ones may be applied).
```php
use DeepCopy\DeepCopy;
use DeepCopy\Filter\ChainableFilter;
use DeepCopy\Filter\Doctrine\DoctrineProxyFilter;
use DeepCopy\Filter\SetNullFilter;
use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;
use DeepCopy\Matcher\PropertyNameMatcher;
$copier = new DeepCopy();
$copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher());
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));
$copy = $copier->copy($object);
echo $copy->id; // null
```
#### `DoctrineCollectionFilter` (filter)
If you use Doctrine and want to copy an entity, you will need to use the `DoctrineCollectionFilter`:
```php
use DeepCopy\DeepCopy;
use DeepCopy\Filter\Doctrine\DoctrineCollectionFilter;
use DeepCopy\Matcher\PropertyTypeMatcher;
$copier = new DeepCopy();
$copier->addFilter(new DoctrineCollectionFilter(), new PropertyTypeMatcher('Doctrine\Common\Collections\Collection'));
$copy = $copier->copy($object);
```
#### `DoctrineEmptyCollectionFilter` (filter)
If you use Doctrine and want to copy an entity who contains a `Collection` that you want to be reset, you can use the
`DoctrineEmptyCollectionFilter`
```php
use DeepCopy\DeepCopy;
use DeepCopy\Filter\Doctrine\DoctrineEmptyCollectionFilter;
use DeepCopy\Matcher\PropertyMatcher;
$copier = new DeepCopy();
$copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher('MyClass', 'myProperty'));
$copy = $copier->copy($object);
// $copy->myProperty will return an empty collection
```
#### `DoctrineProxyFilter` (filter)
If you use Doctrine and use cloning on lazy loaded entities, you might encounter errors mentioning missing fields on a
Doctrine proxy class (...\\\_\_CG\_\_\Proxy).
You can use the `DoctrineProxyFilter` to load the actual entity behind the Doctrine proxy class.
**Make sure, though, to put this as one of your very first filters in the filter chain so that the entity is loaded
before other filters are applied!**
We recommend to decorate the `DoctrineProxyFilter` with the `ChainableFilter` to allow applying other filters to the
cloned lazy loaded entities.
```php
use DeepCopy\DeepCopy;
use DeepCopy\Filter\Doctrine\DoctrineProxyFilter;
use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;
$copier = new DeepCopy();
$copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher());
$copy = $copier->copy($object);
// $copy should now contain a clone of all entities, including those that were not yet fully loaded.
```
#### `ReplaceFilter` (type filter)
1. If you want to replace the value of a property:
```php
use DeepCopy\DeepCopy;
use DeepCopy\Filter\ReplaceFilter;
use DeepCopy\Matcher\PropertyMatcher;
$copier = new DeepCopy();
$callback = function ($currentValue) {
return $currentValue . ' (copy)'
};
$copier->addFilter(new ReplaceFilter($callback), new PropertyMatcher('MyClass', 'title'));
$copy = $copier->copy($object);
// $copy->title will contain the data returned by the callback, e.g. 'The title (copy)'
```
2. If you want to replace whole element:
```php
use DeepCopy\DeepCopy;
use DeepCopy\TypeFilter\ReplaceFilter;
use DeepCopy\TypeMatcher\TypeMatcher;
$copier = new DeepCopy();
$callback = function (MyClass $myClass) {
return get_class($myClass);
};
$copier->addTypeFilter(new ReplaceFilter($callback), new TypeMatcher('MyClass'));
$copy = $copier->copy([new MyClass, 'some string', new MyClass]);
// $copy will contain ['MyClass', 'some string', 'MyClass']
```
The `$callback` parameter of the `ReplaceFilter` constructor accepts any PHP callable.
#### `ShallowCopyFilter` (type filter)
Stop *DeepCopy* from recursively copying element, using standard `clone` instead:
```php
use DeepCopy\DeepCopy;
use DeepCopy\TypeFilter\ShallowCopyFilter;
use DeepCopy\TypeMatcher\TypeMatcher;
use Mockery as m;
$this->deepCopy = new DeepCopy();
$this->deepCopy->addTypeFilter(
new ShallowCopyFilter,
new TypeMatcher(m\MockInterface::class)
);
$myServiceWithMocks = new MyService(m::mock(MyDependency1::class), m::mock(MyDependency2::class));
// All mocks will be just cloned, not deep copied
```
## Edge cases
The following structures cannot be deep-copied with PHP Reflection. As a result they are shallow cloned and filters are
not applied. There is two ways for you to handle them:
- Implement your own `__clone()` method
- Use a filter with a type matcher
## Contributing
DeepCopy is distributed under the MIT license.
### Tests
Running the tests is simple:
```php
vendor/bin/phpunit
```
### Support
Get professional support via [the Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-myclabs-deep-copy?utm_source=packagist-myclabs-deep-copy&utm_medium=referral&utm_campaign=readme).

@ -0,0 +1,42 @@
{
"name": "myclabs/deep-copy",
"description": "Create deep copies (clones) of your objects",
"license": "MIT",
"type": "library",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3,<3.2.2"
},
"autoload": {
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
},
"files": [
"src/DeepCopy/deep_copy.php"
]
},
"autoload-dev": {
"psr-4": {
"DeepCopy\\": "fixtures/",
"DeepCopyTest\\": "tests/DeepCopyTest/"
}
},
"config": {
"sort-packages": true
}
}

@ -0,0 +1,308 @@
<?php
namespace DeepCopy;
use ArrayObject;
use DateInterval;
use DateTimeInterface;
use DateTimeZone;
use DeepCopy\Exception\CloneException;
use DeepCopy\Filter\ChainableFilter;
use DeepCopy\Filter\Filter;
use DeepCopy\Matcher\Matcher;
use DeepCopy\Reflection\ReflectionHelper;
use DeepCopy\TypeFilter\Date\DateIntervalFilter;
use DeepCopy\TypeFilter\Spl\ArrayObjectFilter;
use DeepCopy\TypeFilter\Spl\SplDoublyLinkedListFilter;
use DeepCopy\TypeFilter\TypeFilter;
use DeepCopy\TypeMatcher\TypeMatcher;
use ReflectionObject;
use ReflectionProperty;
use SplDoublyLinkedList;
/**
* @final
*/
class DeepCopy
{
/**
* @var object[] List of objects copied.
*/
private $hashMap = [];
/**
* Filters to apply.
*
* @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
*/
private $filters = [];
/**
* Type Filters to apply.
*
* @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
*/
private $typeFilters = [];
/**
* @var bool
*/
private $skipUncloneable = false;
/**
* @var bool
*/
private $useCloneMethod;
/**
* @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will be used
* instead of the regular deep cloning.
*/
public function __construct($useCloneMethod = false)
{
$this->useCloneMethod = $useCloneMethod;
$this->addTypeFilter(new ArrayObjectFilter($this), new TypeMatcher(ArrayObject::class));
$this->addTypeFilter(new DateIntervalFilter(), new TypeMatcher(DateInterval::class));
$this->addTypeFilter(new SplDoublyLinkedListFilter($this), new TypeMatcher(SplDoublyLinkedList::class));
}
/**
* If enabled, will not throw an exception when coming across an uncloneable property.
*
* @param $skipUncloneable
*
* @return $this
*/
public function skipUncloneable($skipUncloneable = true)
{
$this->skipUncloneable = $skipUncloneable;
return $this;
}
/**
* Deep copies the given object.
*
* @param mixed $object
*
* @return mixed
*/
public function copy($object)
{
$this->hashMap = [];
return $this->recursiveCopy($object);
}
public function addFilter(Filter $filter, Matcher $matcher)
{
$this->filters[] = [
'matcher' => $matcher,
'filter' => $filter,
];
}
public function prependFilter(Filter $filter, Matcher $matcher)
{
array_unshift($this->filters, [
'matcher' => $matcher,
'filter' => $filter,
]);
}
public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher)
{
$this->typeFilters[] = [
'matcher' => $matcher,
'filter' => $filter,
];
}
private function recursiveCopy($var)
{
// Matches Type Filter
if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) {
return $filter->apply($var);
}
// Resource
if (is_resource($var)) {
return $var;
}
// Array
if (is_array($var)) {
return $this->copyArray($var);
}
// Scalar
if (! is_object($var)) {
return $var;
}
// Enum
if (PHP_VERSION_ID >= 80100 && enum_exists(get_class($var))) {
return $var;
}
// Object
return $this->copyObject($var);
}
/**
* Copy an array
* @param array $array
* @return array
*/
private function copyArray(array $array)
{
foreach ($array as $key => $value) {
$array[$key] = $this->recursiveCopy($value);
}
return $array;
}
/**
* Copies an object.
*
* @param object $object
*
* @throws CloneException
*
* @return object
*/
private function copyObject($object)
{
$objectHash = spl_object_hash($object);
if (isset($this->hashMap[$objectHash])) {
return $this->hashMap[$objectHash];
}
$reflectedObject = new ReflectionObject($object);
$isCloneable = $reflectedObject->isCloneable();
if (false === $isCloneable) {
if ($this->skipUncloneable) {
$this->hashMap[$objectHash] = $object;
return $object;
}
throw new CloneException(
sprintf(
'The class "%s" is not cloneable.',
$reflectedObject->getName()
)
);
}
$newObject = clone $object;
$this->hashMap[$objectHash] = $newObject;
if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) {
return $newObject;
}
if ($newObject instanceof DateTimeInterface || $newObject instanceof DateTimeZone) {
return $newObject;
}
foreach (ReflectionHelper::getProperties($reflectedObject) as $property) {
$this->copyObjectProperty($newObject, $property);
}
return $newObject;
}
private function copyObjectProperty($object, ReflectionProperty $property)
{
// Ignore static properties
if ($property->isStatic()) {
return;
}
// Apply the filters
foreach ($this->filters as $item) {
/** @var Matcher $matcher */
$matcher = $item['matcher'];
/** @var Filter $filter */
$filter = $item['filter'];
if ($matcher->matches($object, $property->getName())) {
$filter->apply(
$object,
$property->getName(),
function ($object) {
return $this->recursiveCopy($object);
}
);
if ($filter instanceof ChainableFilter) {
continue;
}
// If a filter matches, we stop processing this property
return;
}
}
$property->setAccessible(true);
// Ignore uninitialized properties (for PHP >7.4)
if (method_exists($property, 'isInitialized') && !$property->isInitialized($object)) {
return;
}
$propertyValue = $property->getValue($object);
// Copy the property
$property->setValue($object, $this->recursiveCopy($propertyValue));
}
/**
* Returns first filter that matches variable, `null` if no such filter found.
*
* @param array $filterRecords Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and
* 'matcher' with value of type {@see TypeMatcher}
* @param mixed $var
*
* @return TypeFilter|null
*/
private function getFirstMatchedTypeFilter(array $filterRecords, $var)
{
$matched = $this->first(
$filterRecords,
function (array $record) use ($var) {
/* @var TypeMatcher $matcher */
$matcher = $record['matcher'];
return $matcher->matches($var);
}
);
return isset($matched) ? $matched['filter'] : null;
}
/**
* Returns first element that matches predicate, `null` if no such element found.
*
* @param array $elements Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
* @param callable $predicate Predicate arguments are: element.
*
* @return array|null Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and 'matcher'
* with value of type {@see TypeMatcher} or `null`.
*/
private function first(array $elements, callable $predicate)
{
foreach ($elements as $element) {
if (call_user_func($predicate, $element)) {
return $element;
}
}
return null;
}
}

@ -0,0 +1,9 @@
<?php
namespace DeepCopy\Exception;
use UnexpectedValueException;
class CloneException extends UnexpectedValueException
{
}

@ -0,0 +1,9 @@
<?php
namespace DeepCopy\Exception;
use ReflectionException;
class PropertyException extends ReflectionException
{
}

@ -0,0 +1,24 @@
<?php
namespace DeepCopy\Filter;
/**
* Defines a decorator filter that will not stop the chain of filters.
*/
class ChainableFilter implements Filter
{
/**
* @var Filter
*/
protected $filter;
public function __construct(Filter $filter)
{
$this->filter = $filter;
}
public function apply($object, $property, $objectCopier)
{
$this->filter->apply($object, $property, $objectCopier);
}
}

@ -0,0 +1,33 @@
<?php
namespace DeepCopy\Filter\Doctrine;
use DeepCopy\Filter\Filter;
use DeepCopy\Reflection\ReflectionHelper;
/**
* @final
*/
class DoctrineCollectionFilter implements Filter
{
/**
* Copies the object property doctrine collection.
*
* {@inheritdoc}
*/
public function apply($object, $property, $objectCopier)
{
$reflectionProperty = ReflectionHelper::getProperty($object, $property);
$reflectionProperty->setAccessible(true);
$oldCollection = $reflectionProperty->getValue($object);
$newCollection = $oldCollection->map(
function ($item) use ($objectCopier) {
return $objectCopier($item);
}
);
$reflectionProperty->setValue($object, $newCollection);
}
}

@ -0,0 +1,28 @@
<?php
namespace DeepCopy\Filter\Doctrine;
use DeepCopy\Filter\Filter;
use DeepCopy\Reflection\ReflectionHelper;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @final
*/
class DoctrineEmptyCollectionFilter implements Filter
{
/**
* Sets the object property to an empty doctrine collection.
*
* @param object $object
* @param string $property
* @param callable $objectCopier
*/
public function apply($object, $property, $objectCopier)
{
$reflectionProperty = ReflectionHelper::getProperty($object, $property);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($object, new ArrayCollection());
}
}

@ -0,0 +1,22 @@
<?php
namespace DeepCopy\Filter\Doctrine;
use DeepCopy\Filter\Filter;
/**
* @final
*/
class DoctrineProxyFilter implements Filter
{
/**
* Triggers the magic method __load() on a Doctrine Proxy class to load the
* actual entity from the database.
*
* {@inheritdoc}
*/
public function apply($object, $property, $objectCopier)
{
$object->__load();
}
}

@ -0,0 +1,18 @@
<?php
namespace DeepCopy\Filter;
/**
* Filter to apply to a property while copying an object
*/
interface Filter
{
/**
* Applies the filter to the object.
*
* @param object $object
* @param string $property
* @param callable $objectCopier
*/
public function apply($object, $property, $objectCopier);
}

@ -0,0 +1,16 @@
<?php
namespace DeepCopy\Filter;
class KeepFilter implements Filter
{
/**
* Keeps the value of the object property.
*
* {@inheritdoc}
*/
public function apply($object, $property, $objectCopier)
{
// Nothing to do
}
}

@ -0,0 +1,39 @@
<?php
namespace DeepCopy\Filter;
use DeepCopy\Reflection\ReflectionHelper;
/**
* @final
*/
class ReplaceFilter implements Filter
{
/**
* @var callable
*/
protected $callback;
/**
* @param callable $callable Will be called to get the new value for each property to replace
*/
public function __construct(callable $callable)
{
$this->callback = $callable;
}
/**
* Replaces the object property by the result of the callback called with the object property.
*
* {@inheritdoc}
*/
public function apply($object, $property, $objectCopier)
{
$reflectionProperty = ReflectionHelper::getProperty($object, $property);
$reflectionProperty->setAccessible(true);
$value = call_user_func($this->callback, $reflectionProperty->getValue($object));
$reflectionProperty->setValue($object, $value);
}
}

@ -0,0 +1,24 @@
<?php
namespace DeepCopy\Filter;
use DeepCopy\Reflection\ReflectionHelper;
/**
* @final
*/
class SetNullFilter implements Filter
{
/**
* Sets the object property to null.
*
* {@inheritdoc}
*/
public function apply($object, $property, $objectCopier)
{
$reflectionProperty = ReflectionHelper::getProperty($object, $property);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($object, null);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save