🚀 🧪 add fit file feature
continuous-integration/drone/push Build is passing Details

issue_028_Import_fit_files
Antoine PEREDERII 1 year ago
parent 5286bdabfb
commit 15e854a508

@ -30,5 +30,8 @@
"scripts": {
"dev": "php -S localhost:8080 -t public -d display_errors=1 -d error_reporting=E_ALL",
"dev:console": "export APP_ENV=console && php public/index.php"
},
"require": {
"adriangibbons/php-fit-file-analysis": "^3.2.0"
}
}

1135
Sources/composer.lock generated

File diff suppressed because it is too large Load Diff

@ -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

@ -1,11 +1,13 @@
Copyright (c) 2018-present Fabien Potencier
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:
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.
@ -15,5 +17,5 @@ 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.
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']);
}
}

File diff suppressed because it is too large Load Diff

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

@ -6,23 +6,16 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'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'),
'adriangibbons\\' => array($vendorDir . '/adriangibbons/php-fit-file-analysis/src'),
'Stub\\' => array($baseDir . '/src/data/stub', $baseDir . '/src/data/stub/service', $baseDir . '/src/data/stub/repository'),
'Shared\\Exception\\' => array($baseDir . '/src/shared/exception'),
'Shared\\' => array($baseDir . '/src/shared'),
'Repository\\' => array($baseDir . '/src/data/model/repository'),
'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'),
'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
'Data\\' => array($baseDir . '/src/data'),
'Console\\' => array($baseDir . '/src/console'),

@ -22,8 +22,6 @@ class ComposerAutoloaderInitb084bad56d99d613841073027e5f5e7e
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInitb084bad56d99d613841073027e5f5e7e', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitb084bad56d99d613841073027e5f5e7e', 'loadClassLoader'));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -3,35 +3,26 @@
'name' => 'hearttrack/package',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'd4345678992503b9eb56ef4afd00ff13f5d7531a',
'reference' => 'f6eafff4cda8cea272db1e1b754a5933d5b7d3ea',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'doctrine/instantiator' => array(
'pretty_version' => '1.5.0',
'version' => '1.5.0.0',
'reference' => '0a0fa9780f5d4e507415a065172d26a98d02047b',
'adriangibbons/php-fit-file-analysis' => array(
'pretty_version' => 'v3.2.4',
'version' => '3.2.4.0',
'reference' => '8efd36b1b963f01c42dc5329626519c040dec664',
'type' => 'library',
'install_path' => __DIR__ . '/../doctrine/instantiator',
'aliases' => array(),
'dev_requirement' => true,
),
'graham-campbell/result-type' => array(
'pretty_version' => 'v1.1.1',
'version' => '1.1.1.0',
'reference' => '672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831',
'type' => 'library',
'install_path' => __DIR__ . '/../graham-campbell/result-type',
'install_path' => __DIR__ . '/../adriangibbons/php-fit-file-analysis',
'aliases' => array(),
'dev_requirement' => false,
),
'hearttrack/package' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'd4345678992503b9eb56ef4afd00ff13f5d7531a',
'reference' => 'f6eafff4cda8cea272db1e1b754a5933d5b7d3ea',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -73,266 +64,203 @@
'aliases' => array(),
'dev_requirement' => true,
),
'phpoption/phpoption' => array(
'pretty_version' => '1.9.1',
'version' => '1.9.1.0',
'reference' => 'dd3a383e599f49777d8b628dadbb90cae435b87e',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoption/phpoption',
'aliases' => array(),
'dev_requirement' => false,
),
'phpunit/php-code-coverage' => array(
'pretty_version' => '9.2.29',
'version' => '9.2.29.0',
'reference' => '6a3a87ac2bbe33b25042753df8195ba4aa534c76',
'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' => '3.0.6',
'version' => '3.0.6.0',
'reference' => 'cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf',
'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' => '3.1.1',
'version' => '3.1.1.0',
'reference' => '5a10147d0aaf65b58940a0b72f71c9ac0423cc67',
'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' => '2.0.4',
'version' => '2.0.4.0',
'reference' => '5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28',
'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' => '5.0.3',
'version' => '5.0.3.0',
'reference' => '5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2',
'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' => '9.6.13',
'version' => '9.6.13.0',
'reference' => 'f3d767f7f9e191eab4189abe41ab37797e30b1be',
'pretty_version' => '10.4.2',
'version' => '10.4.2.0',
'reference' => 'cacd8b9dd224efa8eb28beb69004126c7ca1a1a1',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/phpunit',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/cli-parser' => array(
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'reference' => '442e7c7e687e42adc03470c7b668bc4b2402c0b2',
'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' => '1.0.8',
'version' => '1.0.8.0',
'reference' => '1fc9f64c0927627ef78ba436c9b17d967e68e120',
'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' => '2.0.3',
'version' => '2.0.3.0',
'reference' => 'ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5',
'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' => '4.0.8',
'version' => '4.0.8.0',
'reference' => 'fa0f136dd2334583309d32b62544682ee972b51a',
'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' => '2.0.2',
'version' => '2.0.2.0',
'reference' => '739b35e53379900cc9ac327b2147867b8b6efd88',
'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' => '4.0.5',
'version' => '4.0.5.0',
'reference' => '74be17022044ebaaecfdf0c5cd504fc9cd5a7131',
'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' => '5.1.5',
'version' => '5.1.5.0',
'reference' => '830c43a844f1f8d5b7a1f6d6076b784454d8b7ed',
'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' => '4.0.5',
'version' => '4.0.5.0',
'reference' => 'ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d',
'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' => '5.0.6',
'version' => '5.0.6.0',
'reference' => 'bde739e7565280bda77be70044ac1047bc007e34',
'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' => '1.0.3',
'version' => '1.0.3.0',
'reference' => 'c1c2e997aa3146983ed888ad08b15470a2e22ecc',
'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' => '4.0.4',
'version' => '4.0.4.0',
'reference' => '5c9eeac41b290a3712d88851518825ad78f45c71',
'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' => '2.0.4',
'version' => '2.0.4.0',
'reference' => 'b4f479ebdbf63ac605d183ece17d8d7fe49c15c7',
'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' => '4.0.5',
'version' => '4.0.5.0',
'reference' => 'e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1',
'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/resource-operations' => array(
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'reference' => '0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/resource-operations',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/type' => array(
'pretty_version' => '3.2.1',
'version' => '3.2.1.0',
'reference' => '75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7',
'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' => '3.0.2',
'version' => '3.0.2.0',
'reference' => 'c6c1022351a901512170118436c764e473f6de8c',
'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.1',
'version' => '1.2.1.0',
'reference' => '34a41e998c2183e22995f158c581e7b5e755ab9e',
'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.7.1',
'version' => '3.7.1.0',
'reference' => 'a0ce373a0ca3bf6c64b9e3e2124aca502ba39554',
'type' => 'library',
'install_path' => __DIR__ . '/../twig/twig',
'aliases' => array(),
'dev_requirement' => false,
),
'vlucas/phpdotenv' => array(
'pretty_version' => 'v5.5.0',
'version' => '5.5.0.0',
'reference' => '1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7',
'type' => 'library',
'install_path' => __DIR__ . '/../vlucas/phpdotenv',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

@ -1,26 +0,0 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70205)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". 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
);
}

@ -1,47 +0,0 @@
{
"active": true,
"name": "Instantiator",
"slug": "instantiator",
"docsSlug": "doctrine-instantiator",
"codePath": "/src",
"versions": [
{
"name": "1.5",
"branchName": "1.5.x",
"slug": "latest",
"upcoming": true
},
{
"name": "1.4",
"branchName": "1.4.x",
"slug": "1.4",
"aliases": [
"current",
"stable"
],
"maintained": true,
"current": true
},
{
"name": "1.3",
"branchName": "1.3.x",
"slug": "1.3",
"maintained": false
},
{
"name": "1.2",
"branchName": "1.2.x",
"slug": "1.2"
},
{
"name": "1.1",
"branchName": "1.1.x",
"slug": "1.1"
},
{
"name": "1.0",
"branchName": "1.0.x",
"slug": "1.0"
}
]
}

@ -1,35 +0,0 @@
# Contributing
* Follow the [Doctrine Coding Standard](https://github.com/doctrine/coding-standard)
* The project will follow strict [object calisthenics](http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php)
* Any contribution must provide tests for additional introduced conditions
* Any un-confirmed issue needs a failing test case before being accepted
* Pull requests must be sent from a new hotfix/feature branch, not from `master`.
## Installation
To install the project and run the tests, you need to clone it first:
```sh
$ git clone git://github.com/doctrine/instantiator.git
```
You will then need to run a composer installation:
```sh
$ cd Instantiator
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar update
```
## Testing
The PHPUnit version to be used is the one installed as a dev- dependency via composer:
```sh
$ ./vendor/bin/phpunit
```
Accepted coverage for new contributions is 80%. Any contribution not satisfying this requirement
won't be merged.

@ -1,19 +0,0 @@
Copyright (c) 2014 Doctrine Project
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.

@ -1,38 +0,0 @@
# Instantiator
This library provides a way of avoiding usage of constructors when instantiating PHP classes.
[![Build Status](https://travis-ci.org/doctrine/instantiator.svg?branch=master)](https://travis-ci.org/doctrine/instantiator)
[![Code Coverage](https://codecov.io/gh/doctrine/instantiator/branch/master/graph/badge.svg)](https://codecov.io/gh/doctrine/instantiator/branch/master)
[![Dependency Status](https://www.versioneye.com/package/php--doctrine--instantiator/badge.svg)](https://www.versioneye.com/package/php--doctrine--instantiator)
[![Latest Stable Version](https://poser.pugx.org/doctrine/instantiator/v/stable.png)](https://packagist.org/packages/doctrine/instantiator)
[![Latest Unstable Version](https://poser.pugx.org/doctrine/instantiator/v/unstable.png)](https://packagist.org/packages/doctrine/instantiator)
## Installation
The suggested installation method is via [composer](https://getcomposer.org/):
```sh
composer require doctrine/instantiator
```
## Usage
The instantiator is able to create new instances of any class without using the constructor or any API of the class
itself:
```php
$instantiator = new \Doctrine\Instantiator\Instantiator();
$instance = $instantiator->instantiate(\My\ClassName\Here::class);
```
## Contributing
Please read the [CONTRIBUTING.md](CONTRIBUTING.md) contents if you wish to help out!
## Credits
This library was migrated from [ocramius/instantiator](https://github.com/Ocramius/Instantiator), which
has been donated to the doctrine organization, and which is now deprecated in favour of this package.

@ -1,48 +0,0 @@
{
"name": "doctrine/instantiator",
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
"type": "library",
"license": "MIT",
"homepage": "https://www.doctrine-project.org/projects/instantiator.html",
"keywords": [
"instantiate",
"constructor"
],
"authors": [
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com",
"homepage": "https://ocramius.github.io/"
}
],
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"ext-phar": "*",
"ext-pdo": "*",
"doctrine/coding-standard": "^9 || ^11",
"phpbench/phpbench": "^0.16 || ^1",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-phpunit": "^1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"vimeo/psalm": "^4.30 || ^5.4"
},
"autoload": {
"psr-4": {
"Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
}
},
"autoload-dev": {
"psr-0": {
"DoctrineTest\\InstantiatorPerformance\\": "tests",
"DoctrineTest\\InstantiatorTest\\": "tests",
"DoctrineTest\\InstantiatorTestAsset\\": "tests"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

@ -1,68 +0,0 @@
Introduction
============
This library provides a way of avoiding usage of constructors when instantiating PHP classes.
Installation
============
The suggested installation method is via `composer`_:
.. code-block:: console
$ composer require doctrine/instantiator
Usage
=====
The instantiator is able to create new instances of any class without
using the constructor or any API of the class itself:
.. code-block:: php
<?php
use Doctrine\Instantiator\Instantiator;
use App\Entities\User;
$instantiator = new Instantiator();
$user = $instantiator->instantiate(User::class);
Contributing
============
- Follow the `Doctrine Coding Standard`_
- The project will follow strict `object calisthenics`_
- Any contribution must provide tests for additional introduced
conditions
- Any un-confirmed issue needs a failing test case before being
accepted
- Pull requests must be sent from a new hotfix/feature branch, not from
``master``.
Testing
=======
The PHPUnit version to be used is the one installed as a dev- dependency
via composer:
.. code-block:: console
$ ./vendor/bin/phpunit
Accepted coverage for new contributions is 80%. Any contribution not
satisfying this requirement wont be merged.
Credits
=======
This library was migrated from `ocramius/instantiator`_, which has been
donated to the doctrine organization, and which is now deprecated in
favour of this package.
.. _composer: https://getcomposer.org/
.. _CONTRIBUTING.md: CONTRIBUTING.md
.. _ocramius/instantiator: https://github.com/Ocramius/Instantiator
.. _Doctrine Coding Standard: https://github.com/doctrine/coding-standard
.. _object calisthenics: http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php

@ -1,4 +0,0 @@
.. toctree::
:depth: 3
index

@ -1,16 +0,0 @@
<?xml version="1.0"?>
<psalm
errorLevel="7"
phpVersion="8.2"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>

@ -1,12 +0,0 @@
<?php
namespace Doctrine\Instantiator\Exception;
use Throwable;
/**
* Base exception marker interface for the instantiator component
*/
interface ExceptionInterface extends Throwable
{
}

@ -1,50 +0,0 @@
<?php
namespace Doctrine\Instantiator\Exception;
use InvalidArgumentException as BaseInvalidArgumentException;
use ReflectionClass;
use function interface_exists;
use function sprintf;
use function trait_exists;
/**
* Exception for invalid arguments provided to the instantiator
*/
class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface
{
public static function fromNonExistingClass(string $className): self
{
if (interface_exists($className)) {
return new self(sprintf('The provided type "%s" is an interface, and cannot be instantiated', $className));
}
if (trait_exists($className)) {
return new self(sprintf('The provided type "%s" is a trait, and cannot be instantiated', $className));
}
return new self(sprintf('The provided class "%s" does not exist', $className));
}
/**
* @phpstan-param ReflectionClass<T> $reflectionClass
*
* @template T of object
*/
public static function fromAbstractClass(ReflectionClass $reflectionClass): self
{
return new self(sprintf(
'The provided class "%s" is abstract, and cannot be instantiated',
$reflectionClass->getName()
));
}
public static function fromEnum(string $className): self
{
return new self(sprintf(
'The provided class "%s" is an enum, and cannot be instantiated',
$className
));
}
}

@ -1,59 +0,0 @@
<?php
namespace Doctrine\Instantiator\Exception;
use Exception;
use ReflectionClass;
use UnexpectedValueException as BaseUnexpectedValueException;
use function sprintf;
/**
* Exception for given parameters causing invalid/unexpected state on instantiation
*/
class UnexpectedValueException extends BaseUnexpectedValueException implements ExceptionInterface
{
/**
* @phpstan-param ReflectionClass<T> $reflectionClass
*
* @template T of object
*/
public static function fromSerializationTriggeredException(
ReflectionClass $reflectionClass,
Exception $exception
): self {
return new self(
sprintf(
'An exception was raised while trying to instantiate an instance of "%s" via un-serialization',
$reflectionClass->getName()
),
0,
$exception
);
}
/**
* @phpstan-param ReflectionClass<T> $reflectionClass
*
* @template T of object
*/
public static function fromUncleanUnSerialization(
ReflectionClass $reflectionClass,
string $errorString,
int $errorCode,
string $errorFile,
int $errorLine
): self {
return new self(
sprintf(
'Could not produce an instance of "%s" via un-serialization, since an error was triggered '
. 'in file "%s" at line "%d"',
$reflectionClass->getName(),
$errorFile,
$errorLine
),
0,
new Exception($errorString, $errorCode)
);
}
}

@ -1,262 +0,0 @@
<?php
namespace Doctrine\Instantiator;
use ArrayIterator;
use Doctrine\Instantiator\Exception\ExceptionInterface;
use Doctrine\Instantiator\Exception\InvalidArgumentException;
use Doctrine\Instantiator\Exception\UnexpectedValueException;
use Exception;
use ReflectionClass;
use ReflectionException;
use Serializable;
use function class_exists;
use function enum_exists;
use function is_subclass_of;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function strlen;
use function unserialize;
use const PHP_VERSION_ID;
final class Instantiator implements InstantiatorInterface
{
/**
* Markers used internally by PHP to define whether {@see \unserialize} should invoke
* the method {@see \Serializable::unserialize()} when dealing with classes implementing
* the {@see \Serializable} interface.
*
* @deprecated This constant will be private in 2.0
*/
public const SERIALIZATION_FORMAT_USE_UNSERIALIZER = 'C';
/** @deprecated This constant will be private in 2.0 */
public const SERIALIZATION_FORMAT_AVOID_UNSERIALIZER = 'O';
/**
* Used to instantiate specific classes, indexed by class name.
*
* @var callable[]
*/
private static $cachedInstantiators = [];
/**
* Array of objects that can directly be cloned, indexed by class name.
*
* @var object[]
*/
private static $cachedCloneables = [];
/**
* @param string $className
* @phpstan-param class-string<T> $className
*
* @return object
* @phpstan-return T
*
* @throws ExceptionInterface
*
* @template T of object
*/
public function instantiate($className)
{
if (isset(self::$cachedCloneables[$className])) {
/** @phpstan-var T */
$cachedCloneable = self::$cachedCloneables[$className];
return clone $cachedCloneable;
}
if (isset(self::$cachedInstantiators[$className])) {
$factory = self::$cachedInstantiators[$className];
return $factory();
}
return $this->buildAndCacheFromFactory($className);
}
/**
* Builds the requested object and caches it in static properties for performance
*
* @phpstan-param class-string<T> $className
*
* @return object
* @phpstan-return T
*
* @template T of object
*/
private function buildAndCacheFromFactory(string $className)
{
$factory = self::$cachedInstantiators[$className] = $this->buildFactory($className);
$instance = $factory();
if ($this->isSafeToClone(new ReflectionClass($instance))) {
self::$cachedCloneables[$className] = clone $instance;
}
return $instance;
}
/**
* Builds a callable capable of instantiating the given $className without
* invoking its constructor.
*
* @phpstan-param class-string<T> $className
*
* @phpstan-return callable(): T
*
* @throws InvalidArgumentException
* @throws UnexpectedValueException
* @throws ReflectionException
*
* @template T of object
*/
private function buildFactory(string $className): callable
{
$reflectionClass = $this->getReflectionClass($className);
if ($this->isInstantiableViaReflection($reflectionClass)) {
return [$reflectionClass, 'newInstanceWithoutConstructor'];
}
$serializedString = sprintf(
'%s:%d:"%s":0:{}',
is_subclass_of($className, Serializable::class) ? self::SERIALIZATION_FORMAT_USE_UNSERIALIZER : self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER,
strlen($className),
$className
);
$this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);
return static function () use ($serializedString) {
return unserialize($serializedString);
};
}
/**
* @phpstan-param class-string<T> $className
*
* @phpstan-return ReflectionClass<T>
*
* @throws InvalidArgumentException
* @throws ReflectionException
*
* @template T of object
*/
private function getReflectionClass(string $className): ReflectionClass
{
if (! class_exists($className)) {
throw InvalidArgumentException::fromNonExistingClass($className);
}
if (PHP_VERSION_ID >= 80100 && enum_exists($className, false)) {
throw InvalidArgumentException::fromEnum($className);
}
$reflection = new ReflectionClass($className);
if ($reflection->isAbstract()) {
throw InvalidArgumentException::fromAbstractClass($reflection);
}
return $reflection;
}
/**
* @phpstan-param ReflectionClass<T> $reflectionClass
*
* @throws UnexpectedValueException
*
* @template T of object
*/
private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, string $serializedString): void
{
set_error_handler(static function (int $code, string $message, string $file, int $line) use ($reflectionClass, &$error): bool {
$error = UnexpectedValueException::fromUncleanUnSerialization(
$reflectionClass,
$message,
$code,
$file,
$line
);
return true;
});
try {
$this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
} finally {
restore_error_handler();
}
if ($error) {
throw $error;
}
}
/**
* @phpstan-param ReflectionClass<T> $reflectionClass
*
* @throws UnexpectedValueException
*
* @template T of object
*/
private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, string $serializedString): void
{
try {
unserialize($serializedString);
} catch (Exception $exception) {
throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
}
}
/**
* @phpstan-param ReflectionClass<T> $reflectionClass
*
* @template T of object
*/
private function isInstantiableViaReflection(ReflectionClass $reflectionClass): bool
{
return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
}
/**
* Verifies whether the given class is to be considered internal
*
* @phpstan-param ReflectionClass<T> $reflectionClass
*
* @template T of object
*/
private function hasInternalAncestors(ReflectionClass $reflectionClass): bool
{
do {
if ($reflectionClass->isInternal()) {
return true;
}
$reflectionClass = $reflectionClass->getParentClass();
} while ($reflectionClass);
return false;
}
/**
* Checks if a class is cloneable
*
* Classes implementing `__clone` cannot be safely cloned, as that may cause side-effects.
*
* @phpstan-param ReflectionClass<T> $reflectionClass
*
* @template T of object
*/
private function isSafeToClone(ReflectionClass $reflectionClass): bool
{
return $reflectionClass->isCloneable()
&& ! $reflectionClass->hasMethod('__clone')
&& ! $reflectionClass->isSubclassOf(ArrayIterator::class);
}
}

@ -1,24 +0,0 @@
<?php
namespace Doctrine\Instantiator;
use Doctrine\Instantiator\Exception\ExceptionInterface;
/**
* Instantiator provides utility methods to build objects without invoking their constructors
*/
interface InstantiatorInterface
{
/**
* @param string $className
* @phpstan-param class-string<T> $className
*
* @return object
* @phpstan-return T
*
* @throws ExceptionInterface
*
* @template T of object
*/
public function instantiate($className);
}

@ -1,21 +0,0 @@
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.

@ -1,33 +0,0 @@
{
"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.1"
},
"require-dev": {
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
},
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"GrahamCampbell\\Tests\\ResultType\\": "tests/"
}
},
"config": {
"preferred-install": "dist"
}
}

@ -1,120 +0,0 @@
<?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));
}
}

@ -1,69 +0,0 @@
<?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);
}

@ -1,119 +0,0 @@
<?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);
}
}

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -1,52 +0,0 @@
{
"name": "phpoption/phpoption",
"description": "Option Type for PHP",
"keywords": ["php", "option", "language", "type"],
"license": "Apache-2.0",
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"autoload-dev": {
"psr-4": {
"PhpOption\\Tests\\": "tests/PhpOption/Tests/"
}
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true
},
"preferred-install": "dist"
},
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": true
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

@ -1,175 +0,0 @@
<?php
/*
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace PhpOption;
use Traversable;
/**
* @template T
*
* @extends Option<T>
*/
final class LazyOption extends Option
{
/** @var callable(mixed...):(Option<T>) */
private $callback;
/** @var array<int, mixed> */
private $arguments;
/** @var Option<T>|null */
private $option;
/**
* @template S
* @param callable(mixed...):(Option<S>) $callback
* @param array<int, mixed> $arguments
*
* @return LazyOption<S>
*/
public static function create($callback, array $arguments = []): self
{
return new self($callback, $arguments);
}
/**
* @param callable(mixed...):(Option<T>) $callback
* @param array<int, mixed> $arguments
*/
public function __construct($callback, array $arguments = [])
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException('Invalid callback given');
}
$this->callback = $callback;
$this->arguments = $arguments;
}
public function isDefined(): bool
{
return $this->option()->isDefined();
}
public function isEmpty(): bool
{
return $this->option()->isEmpty();
}
public function get()
{
return $this->option()->get();
}
public function getOrElse($default)
{
return $this->option()->getOrElse($default);
}
public function getOrCall($callable)
{
return $this->option()->getOrCall($callable);
}
public function getOrThrow(\Exception $ex)
{
return $this->option()->getOrThrow($ex);
}
public function orElse(Option $else)
{
return $this->option()->orElse($else);
}
public function ifDefined($callable)
{
$this->option()->forAll($callable);
}
public function forAll($callable)
{
return $this->option()->forAll($callable);
}
public function map($callable)
{
return $this->option()->map($callable);
}
public function flatMap($callable)
{
return $this->option()->flatMap($callable);
}
public function filter($callable)
{
return $this->option()->filter($callable);
}
public function filterNot($callable)
{
return $this->option()->filterNot($callable);
}
public function select($value)
{
return $this->option()->select($value);
}
public function reject($value)
{
return $this->option()->reject($value);
}
/**
* @return Traversable<T>
*/
public function getIterator(): Traversable
{
return $this->option()->getIterator();
}
public function foldLeft($initialValue, $callable)
{
return $this->option()->foldLeft($initialValue, $callable);
}
public function foldRight($initialValue, $callable)
{
return $this->option()->foldRight($initialValue, $callable);
}
/**
* @return Option<T>
*/
private function option(): Option
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);
if ($option instanceof Option) {
$this->option = $option;
} else {
throw new \RuntimeException(sprintf('Expected instance of %s', Option::class));
}
}
return $this->option;
}
}

@ -1,136 +0,0 @@
<?php
/*
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace PhpOption;
use EmptyIterator;
/**
* @extends Option<mixed>
*/
final class None extends Option
{
/** @var None|null */
private static $instance;
/**
* @return None
*/
public static function create(): self
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function get()
{
throw new \RuntimeException('None has no value.');
}
public function getOrCall($callable)
{
return $callable();
}
public function getOrElse($default)
{
return $default;
}
public function getOrThrow(\Exception $ex)
{
throw $ex;
}
public function isEmpty(): bool
{
return true;
}
public function isDefined(): bool
{
return false;
}
public function orElse(Option $else)
{
return $else;
}
public function ifDefined($callable)
{
// Just do nothing in that case.
}
public function forAll($callable)
{
return $this;
}
public function map($callable)
{
return $this;
}
public function flatMap($callable)
{
return $this;
}
public function filter($callable)
{
return $this;
}
public function filterNot($callable)
{
return $this;
}
public function select($value)
{
return $this;
}
public function reject($value)
{
return $this;
}
public function getIterator(): EmptyIterator
{
return new EmptyIterator();
}
public function foldLeft($initialValue, $callable)
{
return $initialValue;
}
public function foldRight($initialValue, $callable)
{
return $initialValue;
}
private function __construct()
{
}
}

@ -1,434 +0,0 @@
<?php
/*
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace PhpOption;
use ArrayAccess;
use IteratorAggregate;
/**
* @template T
*
* @implements IteratorAggregate<T>
*/
abstract class Option implements IteratorAggregate
{
/**
* Creates an option given a return value.
*
* This is intended for consuming existing APIs and allows you to easily
* convert them to an option. By default, we treat ``null`` as the None
* case, and everything else as Some.
*
* @template S
*
* @param S $value The actual return value.
* @param S $noneValue The value which should be considered "None"; null by
* default.
*
* @return Option<S>
*/
public static function fromValue($value, $noneValue = null)
{
if ($value === $noneValue) {
return None::create();
}
return new Some($value);
}
/**
* Creates an option from an array's value.
*
* If the key does not exist in the array, the array is not actually an
* array, or the array's value at the given key is null, None is returned.
* Otherwise, Some is returned wrapping the value at the given key.
*
* @template S
*
* @param array<string|int,S>|ArrayAccess<string|int,S>|null $array A potential array or \ArrayAccess value.
* @param string $key The key to check.
*
* @return Option<S>
*/
public static function fromArraysValue($array, $key)
{
if (!(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) {
return None::create();
}
return new Some($array[$key]);
}
/**
* Creates a lazy-option with the given callback.
*
* This is also a helper constructor for lazy-consuming existing APIs where
* the return value is not yet an option. By default, we treat ``null`` as
* None case, and everything else as Some.
*
* @template S
*
* @param callable $callback The callback to evaluate.
* @param array $arguments The arguments for the callback.
* @param S $noneValue The value which should be considered "None";
* null by default.
*
* @return LazyOption<S>
*/
public static function fromReturn($callback, array $arguments = [], $noneValue = null)
{
return new LazyOption(static function () use ($callback, $arguments, $noneValue) {
/** @var mixed */
$return = call_user_func_array($callback, $arguments);
if ($return === $noneValue) {
return None::create();
}
return new Some($return);
});
}
/**
* Option factory, which creates new option based on passed value.
*
* If value is already an option, it simply returns. If value is callable,
* LazyOption with passed callback created and returned. If Option
* returned from callback, it returns directly. On other case value passed
* to Option::fromValue() method.
*
* @template S
*
* @param Option<S>|callable|S $value
* @param S $noneValue Used when $value is mixed or
* callable, for None-check.
*
* @return Option<S>|LazyOption<S>
*/
public static function ensure($value, $noneValue = null)
{
if ($value instanceof self) {
return $value;
} elseif (is_callable($value)) {
return new LazyOption(static function () use ($value, $noneValue) {
/** @var mixed */
$return = $value();
if ($return instanceof self) {
return $return;
} else {
return self::fromValue($return, $noneValue);
}
});
} else {
return self::fromValue($value, $noneValue);
}
}
/**
* Lift a function so that it accepts Option as parameters.
*
* We return a new closure that wraps the original callback. If any of the
* parameters passed to the lifted function is empty, the function will
* return a value of None. Otherwise, we will pass all parameters to the
* original callback and return the value inside a new Option, unless an
* Option is returned from the function, in which case, we use that.
*
* @template S
*
* @param callable $callback
* @param mixed $noneValue
*
* @return callable
*/
public static function lift($callback, $noneValue = null)
{
return static function () use ($callback, $noneValue) {
/** @var array<int, mixed> */
$args = func_get_args();
$reduced_args = array_reduce(
$args,
/** @param bool $status */
static function ($status, self $o) {
return $o->isEmpty() ? true : $status;
},
false
);
// if at least one parameter is empty, return None
if ($reduced_args) {
return None::create();
}
$args = array_map(
/** @return T */
static function (self $o) {
// it is safe to do so because the fold above checked
// that all arguments are of type Some
/** @var T */
return $o->get();
},
$args
);
return self::ensure(call_user_func_array($callback, $args), $noneValue);
};
}
/**
* Returns the value if available, or throws an exception otherwise.
*
* @throws \RuntimeException If value is not available.
*
* @return T
*/
abstract public function get();
/**
* Returns the value if available, or the default value if not.
*
* @template S
*
* @param S $default
*
* @return T|S
*/
abstract public function getOrElse($default);
/**
* Returns the value if available, or the results of the callable.
*
* This is preferable over ``getOrElse`` if the computation of the default
* value is expensive.
*
* @template S
*
* @param callable():S $callable
*
* @return T|S
*/
abstract public function getOrCall($callable);
/**
* Returns the value if available, or throws the passed exception.
*
* @param \Exception $ex
*
* @return T
*/
abstract public function getOrThrow(\Exception $ex);
/**
* Returns true if no value is available, false otherwise.
*
* @return bool
*/
abstract public function isEmpty();
/**
* Returns true if a value is available, false otherwise.
*
* @return bool
*/
abstract public function isDefined();
/**
* Returns this option if non-empty, or the passed option otherwise.
*
* This can be used to try multiple alternatives, and is especially useful
* with lazy evaluating options:
*
* ```php
* $repo->findSomething()
* ->orElse(new LazyOption(array($repo, 'findSomethingElse')))
* ->orElse(new LazyOption(array($repo, 'createSomething')));
* ```
*
* @param Option<T> $else
*
* @return Option<T>
*/
abstract public function orElse(self $else);
/**
* This is similar to map() below except that the return value has no meaning;
* the passed callable is simply executed if the option is non-empty, and
* ignored if the option is empty.
*
* In all cases, the return value of the callable is discarded.
*
* ```php
* $comment->getMaybeFile()->ifDefined(function($file) {
* // Do something with $file here.
* });
* ```
*
* If you're looking for something like ``ifEmpty``, you can use ``getOrCall``
* and ``getOrElse`` in these cases.
*
* @deprecated Use forAll() instead.
*
* @param callable(T):mixed $callable
*
* @return void
*/
abstract public function ifDefined($callable);
/**
* This is similar to map() except that the return value of the callable has no meaning.
*
* The passed callable is simply executed if the option is non-empty, and ignored if the
* option is empty. This method is preferred for callables with side-effects, while map()
* is intended for callables without side-effects.
*
* @param callable(T):mixed $callable
*
* @return Option<T>
*/
abstract public function forAll($callable);
/**
* Applies the callable to the value of the option if it is non-empty,
* and returns the return value of the callable wrapped in Some().
*
* If the option is empty, then the callable is not applied.
*
* ```php
* (new Some("foo"))->map('strtoupper')->get(); // "FOO"
* ```
*
* @template S
*
* @param callable(T):S $callable
*
* @return Option<S>
*/
abstract public function map($callable);
/**
* Applies the callable to the value of the option if it is non-empty, and
* returns the return value of the callable directly.
*
* In contrast to ``map``, the return value of the callable is expected to
* be an Option itself; it is not automatically wrapped in Some().
*
* @template S
*
* @param callable(T):Option<S> $callable must return an Option
*
* @return Option<S>
*/
abstract public function flatMap($callable);
/**
* If the option is empty, it is returned immediately without applying the callable.
*
* If the option is non-empty, the callable is applied, and if it returns true,
* the option itself is returned; otherwise, None is returned.
*
* @param callable(T):bool $callable
*
* @return Option<T>
*/
abstract public function filter($callable);
/**
* If the option is empty, it is returned immediately without applying the callable.
*
* If the option is non-empty, the callable is applied, and if it returns false,
* the option itself is returned; otherwise, None is returned.
*
* @param callable(T):bool $callable
*
* @return Option<T>
*/
abstract public function filterNot($callable);
/**
* If the option is empty, it is returned immediately.
*
* If the option is non-empty, and its value does not equal the passed value
* (via a shallow comparison ===), then None is returned. Otherwise, the
* Option is returned.
*
* In other words, this will filter all but the passed value.
*
* @param T $value
*
* @return Option<T>
*/
abstract public function select($value);
/**
* If the option is empty, it is returned immediately.
*
* If the option is non-empty, and its value does equal the passed value (via
* a shallow comparison ===), then None is returned; otherwise, the Option is
* returned.
*
* In other words, this will let all values through except the passed value.
*
* @param T $value
*
* @return Option<T>
*/
abstract public function reject($value);
/**
* Binary operator for the initial value and the option's value.
*
* If empty, the initial value is returned. If non-empty, the callable
* receives the initial value and the option's value as arguments.
*
* ```php
*
* $some = new Some(5);
* $none = None::create();
* $result = $some->foldLeft(1, function($a, $b) { return $a + $b; }); // int(6)
* $result = $none->foldLeft(1, function($a, $b) { return $a + $b; }); // int(1)
*
* // This can be used instead of something like the following:
* $option = Option::fromValue($integerOrNull);
* $result = 1;
* if ( ! $option->isEmpty()) {
* $result += $option->get();
* }
* ```
*
* @template S
*
* @param S $initialValue
* @param callable(S, T):S $callable
*
* @return S
*/
abstract public function foldLeft($initialValue, $callable);
/**
* foldLeft() but with reversed arguments for the callable.
*
* @template S
*
* @param S $initialValue
* @param callable(T, S):S $callable
*
* @return S
*/
abstract public function foldRight($initialValue, $callable);
}

@ -1,169 +0,0 @@
<?php
/*
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace PhpOption;
use ArrayIterator;
/**
* @template T
*
* @extends Option<T>
*/
final class Some extends Option
{
/** @var T */
private $value;
/**
* @param T $value
*/
public function __construct($value)
{
$this->value = $value;
}
/**
* @template U
*
* @param U $value
*
* @return Some<U>
*/
public static function create($value): self
{
return new self($value);
}
public function isDefined(): bool
{
return true;
}
public function isEmpty(): bool
{
return false;
}
public function get()
{
return $this->value;
}
public function getOrElse($default)
{
return $this->value;
}
public function getOrCall($callable)
{
return $this->value;
}
public function getOrThrow(\Exception $ex)
{
return $this->value;
}
public function orElse(Option $else)
{
return $this;
}
public function ifDefined($callable)
{
$this->forAll($callable);
}
public function forAll($callable)
{
$callable($this->value);
return $this;
}
public function map($callable)
{
return new self($callable($this->value));
}
public function flatMap($callable)
{
/** @var mixed */
$rs = $callable($this->value);
if (!$rs instanceof Option) {
throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?');
}
return $rs;
}
public function filter($callable)
{
if (true === $callable($this->value)) {
return $this;
}
return None::create();
}
public function filterNot($callable)
{
if (false === $callable($this->value)) {
return $this;
}
return None::create();
}
public function select($value)
{
if ($this->value === $value) {
return $this;
}
return None::create();
}
public function reject($value)
{
if ($this->value === $value) {
return None::create();
}
return $this;
}
/**
* @return ArrayIterator<int, T>
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator([$this->value]);
}
public function foldLeft($initialValue, $callable)
{
return $callable($initialValue, $this->value);
}
public function foldRight($initialValue, $callable)
{
return $callable($this->value, $initialValue);
}
}

@ -0,0 +1,83 @@
# ChangeLog
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [10.1.9] - 2023-11-23
### Fixed
* [#1020](https://github.com/sebastianbergmann/php-code-coverage/issues/1020): Single line method is ignored
## [10.1.8] - 2023-11-15
### Fixed
* [#1018](https://github.com/sebastianbergmann/php-code-coverage/issues/1018): Interface methods are not ignored when their signature is split over multiple lines
## [10.1.7] - 2023-10-04
### Fixed
* [#1014](https://github.com/sebastianbergmann/php-code-coverage/issues/1014): Incorrect statement count in coverage report for constructor property promotion
## [10.1.6] - 2023-09-19
### Fixed
* [#1012](https://github.com/sebastianbergmann/php-code-coverage/issues/1012): Cobertura report pulls functions from report scope, not the individual element
## [10.1.5] - 2023-09-12
### Changed
* [#1011](https://github.com/sebastianbergmann/php-code-coverage/pull/1011): Avoid serialization of cache data in PHP report
## [10.1.4] - 2023-08-31
### Fixed
* Exceptions of type `SebastianBergmann\Template\Exception` are now properly handled
## [10.1.3] - 2023-07-26
### Changed
* The result of `CodeCoverage::getReport()` is now cached
### Fixed
* Static analysis cache keys do not include configuration settings that affect source code parsing
* The Clover, Cobertura, Crap4j, and PHP report writers no longer create a `php:` directory when they should write to `php://stdout`, for instance
## [10.1.2] - 2023-05-22
### Fixed
* [#998](https://github.com/sebastianbergmann/php-code-coverage/pull/998): Group Use Declarations are not handled properly
## [10.1.1] - 2023-04-17
### Fixed
* [#994](https://github.com/sebastianbergmann/php-code-coverage/issues/994): Argument `$linesToBeIgnored` of `CodeCoverage::stop()` has no effect for files that are not executed at all
## [10.1.0] - 2023-04-13
### Added
* [#982](https://github.com/sebastianbergmann/php-code-coverage/issues/982): Add option to ignore lines from code coverage
### Deprecated
* The `SebastianBergmann\CodeCoverage\Filter::includeDirectory()`, `SebastianBergmann\CodeCoverage\Filter::excludeDirectory()`, and `SebastianBergmann\CodeCoverage\Filter::excludeFile()` methods are now deprecated
[10.1.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.8...10.1.9
[10.1.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.7...10.1.8
[10.1.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.6...10.1.7
[10.1.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.5...10.1.6
[10.1.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.4...10.1.5
[10.1.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.3...10.1.4
[10.1.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.2...10.1.3
[10.1.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.1...10.1.2
[10.1.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.0...10.1.1
[10.1.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.0.2...10.1.0

@ -1,563 +0,0 @@
# ChangeLog
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [9.2.29] - 2023-09-19
### Fixed
* [#1012](https://github.com/sebastianbergmann/php-code-coverage/issues/1012): Cobertura report pulls functions from report scope, not the individual element
## [9.2.28] - 2023-09-12
### Changed
* [#1011](https://github.com/sebastianbergmann/php-code-coverage/pull/1011): Avoid serialization of cache data in PHP report
## [9.2.27] - 2023-07-26
### Changed
* The result of `CodeCoverage::getReport()` is now cached
### Fixed
* Static analysis cache keys do not include configuration settings that affect source code parsing
* The Clover, Cobertura, Crap4j, and PHP report writers no longer create a `php:` directory when they should write to `php://stdout`, for instance
## [9.2.26] - 2023-03-06
### Changed
* Improved the legend on the file pages of the HTML code coverage report
## [9.2.25] - 2023-02-25
### Fixed
* [#981](https://github.com/sebastianbergmann/php-code-coverage/issues/981): `CodeUnitFindingVisitor` does not support DNF types
## [9.2.24] - 2023-01-26
### Changed
* [#970](https://github.com/sebastianbergmann/php-code-coverage/issues/970): CSS and JavaScript assets are now referenced using `?v=%s` URLs in the HTML report to avoid cache issues
## [9.2.23] - 2022-12-28
### Fixed
* [#971](https://github.com/sebastianbergmann/php-code-coverage/issues/971): PHP report does not handle serialized code coverage data larger than 2 GB
* [#974](https://github.com/sebastianbergmann/php-code-coverage/issues/974): Executable line analysis fails for declarations with enumerations and unions
## [9.2.22] - 2022-12-18
### Fixed
* [#969](https://github.com/sebastianbergmann/php-code-coverage/pull/969): Fixed identifying line with `throw` as executable
## [9.2.21] - 2022-12-14
### Changed
* [#964](https://github.com/sebastianbergmann/php-code-coverage/pull/964): Changed how executable lines are identified
## [9.2.20] - 2022-12-13
### Fixed
* [#960](https://github.com/sebastianbergmann/php-code-coverage/issues/960): New body font-size is way too big
## [9.2.19] - 2022-11-18
### Fixed
* [#949](https://github.com/sebastianbergmann/php-code-coverage/pull/949): Various issues related to identifying executable lines
### Changed
* Tweaked CSS for HTML report
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.6.2 and jQuery 3.6.1
## [9.2.18] - 2022-10-27
### Fixed
* [#935](https://github.com/sebastianbergmann/php-code-coverage/pull/935): Cobertura package name attribute is always empty
* [#946](https://github.com/sebastianbergmann/php-code-coverage/issues/946): `return` with multiline constant expression must only contain the last line
## [9.2.17] - 2022-08-30
### Changed
* [#928](https://github.com/sebastianbergmann/php-code-coverage/pull/928): Avoid unnecessary `is_file()` calls
* [#931](https://github.com/sebastianbergmann/php-code-coverage/pull/931): Use MD5 instead of CRC32 for static analysis cache file identifier
### Fixed
* [#926](https://github.com/sebastianbergmann/php-code-coverage/pull/926): Static Analysis cache does not work with `open_basedir`
## [9.2.16] - 2022-08-20
### Fixed
* [#926](https://github.com/sebastianbergmann/php-code-coverage/issues/926): File view has wrong colouring for the first column
## [9.2.15] - 2022-03-07
### Fixed
* [#885](https://github.com/sebastianbergmann/php-code-coverage/issues/885): Files that have only `\r` (CR, 0x0d) EOL characters are not handled correctly
* [#907](https://github.com/sebastianbergmann/php-code-coverage/issues/907): Line with only `return [` is not recognized as executable
## [9.2.14] - 2022-02-28
### Fixed
* [#904](https://github.com/sebastianbergmann/php-code-coverage/issues/904): Lines of code containing the `match` keyword were not recognized as executable correctly
* [#905](https://github.com/sebastianbergmann/php-code-coverage/issues/905): Lines of code in constructors were not recognized as executable correctly when constructor property promotion is used
## [9.2.13] - 2022-02-23
### Changed
* The contents of the static analysis sourcecode files is now used to generate the static analysis cache version identifier
### Fixed
* Reverted rename of `SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData` to `SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData` (this class is marked as `@internal` and not covered by the backward compatibility promise, but it is (still) used directly by PHPUnit)
* Reverted rename of `SebastianBergmann\CodeCoverage\RawCodeCoverageData` to `SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData` (this class is marked as `@internal` and not covered by the backward compatibility promise, but it is (still) used directly by PHPUnit)
* The `ArrayDim`, `Cast`, and `MethodCall` nodes are now considered when determining whether a line of code is executable or not
## [9.2.12] - 2022-02-23 [YANKED]
### Changed
* [#898](https://github.com/sebastianbergmann/php-code-coverage/pull/898): Use content hash instead of `filemtime()` to determine cache hit/miss
### Fixed
* [#736](https://github.com/sebastianbergmann/php-code-coverage/issues/736): HTML report generator allows invalid values for low upper bound and high lower bound
* [#854](https://github.com/sebastianbergmann/php-code-coverage/issues/854): "Class Coverage Distribution" and "Class Complexity" graphs are not displayed at full width
* [#897](https://github.com/sebastianbergmann/php-code-coverage/issues/897): `declare(strict_types=1)` marked as uncovered
## [9.2.11] - 2022-02-18
### Changed
* `CoveredFileAnalyser` and `UncoveredFileAnalyser` have been combined to `FileAnalyser`
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.6.1, jQuery 3.6.0, and popper.js 1.16.1
### Fixed
* [#889](https://github.com/sebastianbergmann/php-code-coverage/issues/889): Code Coverage depends on autoload order
## [9.2.10] - 2021-12-05
### Fixed
* [#887](https://github.com/sebastianbergmann/php-code-coverage/issues/887): Document return type of `CodeUnitFindingVisitor::enterNode()` so that Symfony's DebugClassLoader does not trigger a deprecation warning
## [9.2.9] - 2021-11-19
### Fixed
* [#882](https://github.com/sebastianbergmann/php-code-coverage/issues/882): PHPUnit 9.2.8 has wrong version number
## [9.2.8] - 2021-10-30
### Fixed
* [#866](https://github.com/sebastianbergmann/php-code-coverage/issues/866): `CodeUnitFindingVisitor` does not handle `enum` type introduced in PHP 8.1
* [#868](https://github.com/sebastianbergmann/php-code-coverage/pull/868): Uncovered files should be ignored unless requested
* [#876](https://github.com/sebastianbergmann/php-code-coverage/issues/876): PCOV driver causes 2x slowdown after upgrade to PHPUnit 9.5
## [9.2.7] - 2021-09-17
### Fixed
* [#860](https://github.com/sebastianbergmann/php-code-coverage/pull/860): Empty value for `XDEBUG_MODE` environment variable is not handled correctly
## [9.2.6] - 2021-03-28
### Fixed
* [#846](https://github.com/sebastianbergmann/php-code-coverage/issues/846): Method name should not appear in the method signature attribute of Cobertura XML
## [9.2.5] - 2020-11-28
### Fixed
* [#831](https://github.com/sebastianbergmann/php-code-coverage/issues/831): Files that do not contain a newline are not handled correctly
## [9.2.4] - 2020-11-27
### Added
* [#834](https://github.com/sebastianbergmann/php-code-coverage/issues/834): Support `XDEBUG_MODE` environment variable
## [9.2.3] - 2020-10-30
### Changed
* Bumped required version of `nikic/php-parser`
## [9.2.2] - 2020-10-28
### Fixed
* [#820](https://github.com/sebastianbergmann/php-code-coverage/issues/820): Hidden dependency on PHPUnit
## [9.2.1] - 2020-10-26
### Fixed
* `SebastianBergmann\CodeCoverage\Exception` now correctly extends `\Throwable`
## [9.2.0] - 2020-10-02
### Added
* [#812](https://github.com/sebastianbergmann/php-code-coverage/pull/812): Support for Cobertura XML report format
### Changed
* Reduced the number of I/O operations performed by the static analysis cache
## [9.1.11] - 2020-09-19
### Fixed
* [#811](https://github.com/sebastianbergmann/php-code-coverage/issues/811): `T_FN` constant is used on PHP 7.3 where it is not available
## [9.1.10] - 2020-09-18
### Added
* `SebastianBergmann\CodeCoverage\Driver\Selector::forLineCoverage()` and `SebastianBergmann\CodeCoverage\Driver\Selector::forLineAndPathCoverage()` have been added
### Fixed
* [#810](https://github.com/sebastianbergmann/php-code-coverage/issues/810): `SebastianBergmann\CodeCoverage\Driver\Driver::forLineCoverage()` and `SebastianBergmann\CodeCoverage\Driver\Driver::forLineAndPathCoverage()` are marked as internal
### Removed
* `SebastianBergmann\CodeCoverage\Driver\Driver::forLineCoverage()` and `SebastianBergmann\CodeCoverage\Driver\Driver::forLineAndPathCoverage()` are now deprecated
## [9.1.9] - 2020-09-15
### Fixed
* [#808](https://github.com/sebastianbergmann/php-code-coverage/issues/808): `PHP Warning: Use of undefined constant T_MATCH`
## [9.1.8] - 2020-09-07
### Changed
* [#800](https://github.com/sebastianbergmann/php-code-coverage/pull/800): All files on the inclusion list are no longer loaded when `SebastianBergmann\CodeCoverage::start()` is called for the first time and `processUncoveredFiles` is set to `true`
### Fixed
* [#799](https://github.com/sebastianbergmann/php-code-coverage/issues/799): Uncovered new line at end of file
## [9.1.7] - 2020-09-03
### Fixed
* Fixed regressions introduced in versions 9.1.5 and 9.1.6
## [9.1.6] - 2020-08-31
### Fixed
* [#799](https://github.com/sebastianbergmann/php-code-coverage/issues/799): Uncovered new line at end of file
* [#803](https://github.com/sebastianbergmann/php-code-coverage/issues/803): HTML report does not sort directories and files anymore
## [9.1.5] - 2020-08-27
### Changed
* [#800](https://github.com/sebastianbergmann/php-code-coverage/pull/800): All files on the inclusion list are no longer loaded when `SebastianBergmann\CodeCoverage::start()` is called for the first time and `processUncoveredFiles` is set to `true`
### Fixed
* [#797](https://github.com/sebastianbergmann/php-code-coverage/pull/797): Class name is wrongly removed from namespace name
## [9.1.4] - 2020-08-13
### Fixed
* [#793](https://github.com/sebastianbergmann/php-code-coverage/issues/793): Lines with `::class` constant are not covered
## [9.1.3] - 2020-08-10
### Changed
* Changed PHP-Parser usage to parse sourcecode according to the PHP version we are currently running on instead of using emulative lexing
## [9.1.2] - 2020-08-10
### Fixed
* [#791](https://github.com/sebastianbergmann/php-code-coverage/pull/791): Cache Warmer does not warm all caches
## [9.1.1] - 2020-08-10
### Added
* Added `SebastianBergmann\CodeCoverage::cacheDirectory()` method for querying where the cache writes its files
## [9.1.0] - 2020-08-10
### Added
* Implemented a persistent cache for information gathered using PHP-Parser based static analysis (hereinafter referred to as "cache")
* Added `SebastianBergmann\CodeCoverage::cacheStaticAnalysis(string $cacheDirectory)` method for enabling the cache; it will write its files to `$directory`
* Added `SebastianBergmann\CodeCoverage::doNotCacheStaticAnalysis` method for disabling the cache
* Added `SebastianBergmann\CodeCoverage::cachesStaticAnalysis()` method for querying whether the cache is enabled
* Added `SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer::warmCache()` method for warming the cache
## [9.0.0] - 2020-08-07
### Added
* [#761](https://github.com/sebastianbergmann/php-code-coverage/pull/761): Support for Branch Coverage and Path Coverage
* Added `SebastianBergmann\CodeCoverage\Driver\Driver::forLineCoverage()` for selecting the best available driver for line coverage
* Added `SebastianBergmann\CodeCoverage\Driver\Driver::forLineAndPathCoverage()` for selecting the best available driver for path coverage
* This component is now supported on PHP 8
* This component now supports Xdebug 3
### Changed
* [#746](https://github.com/sebastianbergmann/php-code-coverage/pull/746): Remove some ancient workarounds for very old Xdebug versions
* [#747](https://github.com/sebastianbergmann/php-code-coverage/pull/747): Use native filtering in PCOV and Xdebug drivers
* [#748](https://github.com/sebastianbergmann/php-code-coverage/pull/748): Store raw code coverage in value objects instead of arrays
* [#749](https://github.com/sebastianbergmann/php-code-coverage/pull/749): Store processed code coverage in value objects instead of arrays
* [#752](https://github.com/sebastianbergmann/php-code-coverage/pull/752): Rework how code coverage settings are propagated to the driver
* [#754](https://github.com/sebastianbergmann/php-code-coverage/pull/754): Implement collection of raw branch and path coverage
* [#755](https://github.com/sebastianbergmann/php-code-coverage/pull/755): Implement processing of raw branch and path coverage
* [#756](https://github.com/sebastianbergmann/php-code-coverage/pull/756): Improve handling of uncovered files
* `SebastianBergmann\CodeCoverage\Filter::addDirectoryToWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::includeDirectory()`
* `SebastianBergmann\CodeCoverage\Filter::addFilesToWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::includeFiles()`
* `SebastianBergmann\CodeCoverage\Filter::addFileToWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::includeFile()`
* `SebastianBergmann\CodeCoverage\Filter::removeDirectoryFromWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::excludeDirectory()`
* `SebastianBergmann\CodeCoverage\Filter::removeFileFromWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::excludeFile()`
* `SebastianBergmann\CodeCoverage\Filter::isFiltered()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::isExcluded()`
* `SebastianBergmann\CodeCoverage\Filter::getWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::files()`
* The arguments for `CodeCoverage::__construct()` are no longer optional
### Fixed
* [#700](https://github.com/sebastianbergmann/php-code-coverage/pull/700): Throw an exception if code coverage fails to write to disk
### Removed
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCacheTokens()` and `SebastianBergmann\CodeCoverage\CodeCoverage::getCacheTokens()` have been removed
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCheckForUnintentionallyCoveredCode()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::enableCheckForUnintentionallyCoveredCode()` or `SebastianBergmann\CodeCoverage\CodeCoverage::disableCheckForUnintentionallyCoveredCode()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setSubclassesExcludedFromUnintentionallyCoveredCodeCheck()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setAddUncoveredFilesFromWhitelist()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::includeUncoveredFiles()` or `SebastianBergmann\CodeCoverage\CodeCoverage::excludeUncoveredFiles()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setProcessUncoveredFiles()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::processUncoveredFiles()` or `SebastianBergmann\CodeCoverage\CodeCoverage::doNotProcessUncoveredFiles()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setIgnoreDeprecatedCode()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::ignoreDeprecatedCode()` or `SebastianBergmann\CodeCoverage\CodeCoverage::doNotIgnoreDeprecatedCode()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setDisableIgnoredLines()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::enableAnnotationsForIgnoringCode()` or `SebastianBergmann\CodeCoverage\CodeCoverage::disableAnnotationsForIgnoringCode()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCheckForMissingCoversAnnotation()` has been removed
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCheckForUnexecutedCoveredCode()` has been removed
* `SebastianBergmann\CodeCoverage\CodeCoverage::setForceCoversAnnotation()` has been removed
* `SebastianBergmann\CodeCoverage\Filter::hasWhitelist()` has been removed, please use `SebastianBergmann\CodeCoverage\Filter::isEmpty()` instead
* `SebastianBergmann\CodeCoverage\Filter::getWhitelistedFiles()` has been removed
* `SebastianBergmann\CodeCoverage\Filter::setWhitelistedFiles()` has been removed
## [8.0.2] - 2020-05-23
### Fixed
* [#750](https://github.com/sebastianbergmann/php-code-coverage/pull/750): Inconsistent handling of namespaces
* [#751](https://github.com/sebastianbergmann/php-code-coverage/pull/751): Dead code is not highlighted correctly
* [#753](https://github.com/sebastianbergmann/php-code-coverage/issues/753): Do not use `$_SERVER['REQUEST_TIME']` because the test(ed) code might unset it
## [8.0.1] - 2020-02-19
### Fixed
* [#731](https://github.com/sebastianbergmann/php-code-coverage/pull/731): Confusing footer in the HTML report
## [8.0.0] - 2020-02-07
### Fixed
* [#721](https://github.com/sebastianbergmann/php-code-coverage/pull/721): Workaround for PHP bug [#79191](https://bugs.php.net/bug.php?id=79191)
### Removed
* This component is no longer supported on PHP 7.2
## [7.0.15] - 2021-07-26
### Changed
* Bumped required version of php-token-stream
## [7.0.14] - 2020-12-02
### Changed
* [#837](https://github.com/sebastianbergmann/php-code-coverage/issues/837): Allow version 4 of php-token-stream
## [7.0.13] - 2020-11-30
### Changed
* Changed PHP version constraint in `composer.json` from `^7.2` to `>=7.2` to allow installation of this version of this library on PHP 8. However, this version of this library does not work on PHP 8. PHPUnit 8.5, which uses this version of this library, does not call into this library and instead shows a message that code coverage functionality is not available for PHPUnit 8.5 on PHP 8.
## [7.0.12] - 2020-11-27
### Added
* [#834](https://github.com/sebastianbergmann/php-code-coverage/issues/834): Support `XDEBUG_MODE` environment variable
## [7.0.11] - 2020-11-27
### Added
* Support for Xdebug 3
## [7.0.10] - 2019-11-20
### Fixed
* [#710](https://github.com/sebastianbergmann/php-code-coverage/pull/710): Code Coverage does not work in PhpStorm
## [7.0.9] - 2019-11-20
### Changed
* [#709](https://github.com/sebastianbergmann/php-code-coverage/pull/709): Prioritize PCOV over Xdebug
## [7.0.8] - 2019-09-17
### Changed
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.3.1, jQuery 3.4.1, and popper.js 1.15.0
## [7.0.7] - 2019-07-25
### Changed
* Bumped required version of php-token-stream
## [7.0.6] - 2019-07-08
### Changed
* Bumped required version of php-token-stream
## [7.0.5] - 2019-06-06
### Fixed
* [#681](https://github.com/sebastianbergmann/php-code-coverage/pull/681): `use function` statements are not ignored
## [7.0.4] - 2019-05-29
### Fixed
* [#682](https://github.com/sebastianbergmann/php-code-coverage/pull/682): Code that is not executed is reported as being executed when using PCOV
## [7.0.3] - 2019-02-26
### Fixed
* [#671](https://github.com/sebastianbergmann/php-code-coverage/issues/671): `TypeError` when directory name is a number
## [7.0.2] - 2019-02-15
### Changed
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.3.0
### Fixed
* [#667](https://github.com/sebastianbergmann/php-code-coverage/pull/667): `TypeError` in PHP reporter
## [7.0.1] - 2019-02-01
### Fixed
* [#664](https://github.com/sebastianbergmann/php-code-coverage/issues/664): `TypeError` when whitelisted file does not exist
## [7.0.0] - 2019-02-01
### Added
* [#663](https://github.com/sebastianbergmann/php-code-coverage/pull/663): Support for PCOV
### Fixed
* [#654](https://github.com/sebastianbergmann/php-code-coverage/issues/654): HTML report fails to load assets
* [#655](https://github.com/sebastianbergmann/php-code-coverage/issues/655): Popin pops in outside of screen
### Removed
* This component is no longer supported on PHP 7.1
[9.2.29]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.28...9.2.29
[9.2.28]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.27...9.2.28
[9.2.27]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.26...9.2.27
[9.2.26]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.25...9.2.26
[9.2.25]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.24...9.2.25
[9.2.24]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.23...9.2.24
[9.2.23]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.22...9.2.23
[9.2.22]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.21...9.2.22
[9.2.21]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.20...9.2.21
[9.2.20]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.19...9.2.20
[9.2.19]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.18...9.2.19
[9.2.18]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.17...9.2.18
[9.2.17]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.16...9.2.17
[9.2.16]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.15...9.2.16
[9.2.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.14...9.2.15
[9.2.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.13...9.2.14
[9.2.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/c011a0b6aaa4acd2f39b7f51fb4ad4442b6ec631...9.2.13
[9.2.12]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.11...c011a0b6aaa4acd2f39b7f51fb4ad4442b6ec631
[9.2.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.10...9.2.11
[9.2.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.9...9.2.10
[9.2.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.8...9.2.9
[9.2.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.7...9.2.8
[9.2.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.6...9.2.7
[9.2.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.5...9.2.6
[9.2.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.4...9.2.5
[9.2.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.3...9.2.4
[9.2.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.2...9.2.3
[9.2.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.1...9.2.2
[9.2.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.0...9.2.1
[9.2.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.11...9.2.0
[9.1.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.10...9.1.11
[9.1.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.9...9.1.10
[9.1.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.8...9.1.9
[9.1.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.7...9.1.8
[9.1.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.6...9.1.7
[9.1.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.5...9.1.6
[9.1.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.4...9.1.5
[9.1.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.3...9.1.4
[9.1.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.2...9.1.3
[9.1.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.1...9.1.2
[9.1.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.0...9.1.1
[9.1.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.0.0...9.1.0
[9.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/8.0...9.0.0
[8.0.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/8.0.1...8.0.2
[8.0.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/8.0.0...8.0.1
[8.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.10...8.0.0
[7.0.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.14...7.0.15
[7.0.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.13...7.0.14
[7.0.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.12...7.0.13
[7.0.12]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.11...7.0.12
[7.0.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.10...7.0.11
[7.0.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.9...7.0.10
[7.0.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.8...7.0.9
[7.0.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.7...7.0.8
[7.0.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.6...7.0.7
[7.0.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.5...7.0.6
[7.0.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.4...7.0.5
[7.0.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.3...7.0.4
[7.0.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.2...7.0.3
[7.0.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.1...7.0.2
[7.0.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.0...7.0.1
[7.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/6.1.4...7.0.0

@ -3,6 +3,7 @@
[![Latest Stable Version](https://poser.pugx.org/phpunit/php-code-coverage/v/stable.png)](https://packagist.org/packages/phpunit/php-code-coverage)
[![CI Status](https://github.com/sebastianbergmann/php-code-coverage/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-code-coverage/actions)
[![Type Coverage](https://shepherd.dev/github/sebastianbergmann/php-code-coverage/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/php-code-coverage)
[![codecov](https://codecov.io/gh/sebastianbergmann/php-code-coverage/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-code-coverage)
Provides collection, processing, and rendering functionality for PHP code coverage information.
@ -30,7 +31,13 @@ use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Report\Html\Facade as HtmlReport;
$filter = new Filter;
$filter->includeDirectory('/path/to/directory');
$filter->includeFiles(
[
'/path/to/file.php',
'/path/to/another_file.php',
]
);
$coverage = new CodeCoverage(
(new Selector)->forLineCoverage($filter),

@ -0,0 +1,30 @@
# Security Policy
If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure.
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
Instead, please email `sebastian@phpunit.de`.
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
* The type of issue
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
## Web Context
The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit.
The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes.
If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context.
Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes.

@ -22,29 +22,29 @@
},
"config": {
"platform": {
"php": "7.3.0"
"php": "8.1.0"
},
"optimize-autoloader": true,
"sort-packages": true
},
"prefer-stable": true,
"require": {
"php": ">=7.3",
"php": ">=8.1",
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.15",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
"sebastian/code-unit-reverse-lookup": "^2.0.2",
"sebastian/complexity": "^2.0",
"sebastian/environment": "^5.1.2",
"sebastian/lines-of-code": "^1.0.3",
"sebastian/version": "^3.0.1",
"phpunit/php-file-iterator": "^4.0",
"phpunit/php-text-template": "^3.0",
"sebastian/code-unit-reverse-lookup": "^3.0",
"sebastian/complexity": "^3.0",
"sebastian/environment": "^6.0",
"sebastian/lines-of-code": "^2.0",
"sebastian/version": "^4.0",
"theseer/tokenizer": "^1.2.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3"
"phpunit/phpunit": "^10.1"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@ -63,7 +63,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "9.2-dev"
"dev-main": "10.1-dev"
}
}
}

@ -14,110 +14,65 @@ use function array_diff_key;
use function array_flip;
use function array_keys;
use function array_merge;
use function array_merge_recursive;
use function array_unique;
use function array_values;
use function count;
use function explode;
use function get_class;
use function is_array;
use function is_file;
use function sort;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\PhptTestCase;
use PHPUnit\Util\Test;
use ReflectionClass;
use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\Node\Builder;
use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingFileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;
use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize;
use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus;
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
/**
* Provides collection functionality for PHP code coverage information.
*
* @psalm-type TestType = array{
* size: string,
* status: string,
* }
*/
final class CodeCoverage
{
private const UNCOVERED_FILES = 'UNCOVERED_FILES';
private readonly Driver $driver;
private readonly Filter $filter;
private readonly Wizard $wizard;
private bool $checkForUnintentionallyCoveredCode = false;
private bool $includeUncoveredFiles = true;
private bool $ignoreDeprecatedCode = false;
private ?string $currentId = null;
private ?TestSize $currentSize = null;
private ProcessedCodeCoverageData $data;
private bool $useAnnotationsForIgnoringCode = true;
/**
* @var Driver
*/
private $driver;
/**
* @var Filter
*/
private $filter;
/**
* @var Wizard
* @psalm-var array<string,list<int>>
*/
private $wizard;
private array $linesToBeIgnored = [];
/**
* @var bool
* @psalm-var array<string, TestType>
*/
private $checkForUnintentionallyCoveredCode = false;
/**
* @var bool
*/
private $includeUncoveredFiles = true;
/**
* @var bool
*/
private $processUncoveredFiles = false;
/**
* @var bool
*/
private $ignoreDeprecatedCode = false;
/**
* @var null|PhptTestCase|string|TestCase
*/
private $currentId;
/**
* Code coverage data.
*
* @var ProcessedCodeCoverageData
*/
private $data;
/**
* @var bool
*/
private $useAnnotationsForIgnoringCode = true;
/**
* Test data.
*
* @var array
*/
private $tests = [];
private array $tests = [];
/**
* @psalm-var list<class-string>
*/
private $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
/**
* @var ?FileAnalyser
*/
private $analyser;
/**
* @var ?string
*/
private $cacheDirectory;
/**
* @var ?Directory
*/
private $cachedReport;
private array $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
private ?FileAnalyser $analyser = null;
private ?string $cacheDirectory = null;
private ?Directory $cachedReport = null;
public function __construct(Driver $driver, Filter $filter)
{
@ -145,6 +100,7 @@ final class CodeCoverage
public function clear(): void
{
$this->currentId = null;
$this->currentSize = null;
$this->data = new ProcessedCodeCoverageData;
$this->tests = [];
$this->cachedReport = null;
@ -172,9 +128,7 @@ final class CodeCoverage
public function getData(bool $raw = false): ProcessedCodeCoverageData
{
if (!$raw) {
if ($this->processUncoveredFiles) {
$this->processUncoveredFilesFromFilter();
} elseif ($this->includeUncoveredFiles) {
if ($this->includeUncoveredFiles) {
$this->addUncoveredFilesFromFilter();
}
}
@ -191,7 +145,7 @@ final class CodeCoverage
}
/**
* Returns the test data.
* @psalm-return array<string, TestType>
*/
public function getTests(): array
{
@ -199,25 +153,21 @@ final class CodeCoverage
}
/**
* Sets the test data.
* @psalm-param array<string, TestType> $tests
*/
public function setTests(array $tests): void
{
$this->tests = $tests;
}
/**
* Start collection of code coverage information.
*
* @param PhptTestCase|string|TestCase $id
*/
public function start($id, bool $clear = false): void
public function start(string $id, TestSize $size = null, bool $clear = false): void
{
if ($clear) {
$this->clear();
}
$this->currentId = $id;
$this->currentId = $id;
$this->currentSize = $size;
$this->driver->start();
@ -225,38 +175,34 @@ final class CodeCoverage
}
/**
* Stop collection of code coverage information.
*
* @param array|false $linesToBeCovered
* @psalm-param array<string,list<int>> $linesToBeIgnored
*/
public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): RawCodeCoverageData
public function stop(bool $append = true, TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []): RawCodeCoverageData
{
if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
throw new InvalidArgumentException(
'$linesToBeCovered must be an array or false'
);
}
$data = $this->driver->stop();
$this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
$this->linesToBeIgnored = array_merge_recursive(
$this->linesToBeIgnored,
$linesToBeIgnored
);
$this->append($data, null, $append, $status, $linesToBeCovered, $linesToBeUsed, $linesToBeIgnored);
$this->currentId = null;
$this->currentSize = null;
$this->cachedReport = null;
return $data;
}
/**
* Appends code coverage data.
*
* @param PhptTestCase|string|TestCase $id
* @param array|false $linesToBeCovered
* @psalm-param array<string,list<int>> $linesToBeIgnored
*
* @throws ReflectionException
* @throws TestIdMissingException
* @throws UnintentionallyCoveredCodeException
*/
public function append(RawCodeCoverageData $rawData, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): void
public function append(RawCodeCoverageData $rawData, string $id = null, bool $append = true, TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []): void
{
if ($id === null) {
$id = $this->currentId;
@ -268,12 +214,22 @@ final class CodeCoverage
$this->cachedReport = null;
if ($status === null) {
$status = TestStatus::unknown();
}
$size = $this->currentSize;
if ($size === null) {
$size = TestSize::unknown();
}
$this->applyFilter($rawData);
$this->applyExecutableLinesFilter($rawData);
if ($this->useAnnotationsForIgnoringCode) {
$this->applyIgnoredLinesFilter($rawData);
$this->applyIgnoredLinesFilter($rawData, $linesToBeIgnored);
}
$this->data->initializeUnseenData($rawData);
@ -282,45 +238,27 @@ final class CodeCoverage
return;
}
if ($id !== self::UNCOVERED_FILES) {
$this->applyCoversAnnotationFilter(
$rawData,
$linesToBeCovered,
$linesToBeUsed
);
if (empty($rawData->lineCoverage())) {
return;
}
$size = 'unknown';
$status = -1;
$fromTestcase = false;
if ($id instanceof TestCase) {
$fromTestcase = true;
$_size = $id->getSize();
if ($id === self::UNCOVERED_FILES) {
return;
}
if ($_size === Test::SMALL) {
$size = 'small';
} elseif ($_size === Test::MEDIUM) {
$size = 'medium';
} elseif ($_size === Test::LARGE) {
$size = 'large';
}
$this->applyCoversAndUsesFilter(
$rawData,
$linesToBeCovered,
$linesToBeUsed,
$size
);
$status = $id->getStatus();
$id = get_class($id) . '::' . $id->getName();
} elseif ($id instanceof PhptTestCase) {
$fromTestcase = true;
$size = 'large';
$id = $id->getName();
}
if (empty($rawData->lineCoverage())) {
return;
}
$this->tests[$id] = ['size' => $size, 'status' => $status, 'fromTestcase' => $fromTestcase];
$this->tests[$id] = [
'size' => $size->asString(),
'status' => $status->asString(),
];
$this->data->markCodeAsExecutedByTestCase($id, $rawData);
}
$this->data->markCodeAsExecutedByTestCase($id, $rawData);
}
/**
@ -359,16 +297,6 @@ final class CodeCoverage
$this->includeUncoveredFiles = false;
}
public function processUncoveredFiles(): void
{
$this->processUncoveredFiles = true;
}
public function doNotProcessUncoveredFiles(): void
{
$this->processUncoveredFiles = false;
}
public function enableAnnotationsForIgnoringCode(): void
{
$this->useAnnotationsForIgnoringCode = true;
@ -450,14 +378,10 @@ final class CodeCoverage
}
/**
* Applies the @covers annotation filtering.
*
* @param array|false $linesToBeCovered
*
* @throws ReflectionException
* @throws UnintentionallyCoveredCodeException
*/
private function applyCoversAnnotationFilter(RawCodeCoverageData $rawData, $linesToBeCovered, array $linesToBeUsed): void
private function applyCoversAndUsesFilter(RawCodeCoverageData $rawData, array|false $linesToBeCovered, array $linesToBeUsed, TestSize $size): void
{
if ($linesToBeCovered === false) {
$rawData->clear();
@ -469,9 +393,7 @@ final class CodeCoverage
return;
}
if ($this->checkForUnintentionallyCoveredCode &&
(!$this->currentId instanceof TestCase ||
(!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
if ($this->checkForUnintentionallyCoveredCode && !$size->isMedium() && !$size->isLarge()) {
$this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed);
}
@ -524,13 +446,23 @@ final class CodeCoverage
}
}
private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void
/**
* @psalm-param array<string,list<int>> $linesToBeIgnored
*/
private function applyIgnoredLinesFilter(RawCodeCoverageData $data, array $linesToBeIgnored): void
{
foreach (array_keys($data->lineCoverage()) as $filename) {
if (!$this->filter->isFile($filename)) {
continue;
}
if (isset($linesToBeIgnored[$filename])) {
$data->removeCoverageDataForLines(
$filename,
$linesToBeIgnored[$filename]
);
}
$data->removeCoverageDataForLines(
$filename,
$this->analyser()->ignoredLinesFor($filename)
@ -549,39 +481,19 @@ final class CodeCoverage
);
foreach ($uncoveredFiles as $uncoveredFile) {
if ($this->filter->isFile($uncoveredFile)) {
if (is_file($uncoveredFile)) {
$this->append(
RawCodeCoverageData::fromUncoveredFile(
$uncoveredFile,
$this->analyser()
),
self::UNCOVERED_FILES
self::UNCOVERED_FILES,
linesToBeIgnored: $this->linesToBeIgnored
);
}
}
}
/**
* @throws UnintentionallyCoveredCodeException
*/
private function processUncoveredFilesFromFilter(): void
{
$uncoveredFiles = array_diff(
$this->filter->files(),
$this->data->coveredFiles()
);
$this->driver->start();
foreach ($uncoveredFiles as $uncoveredFile) {
if ($this->filter->isFile($uncoveredFile)) {
include_once $uncoveredFile;
}
}
$this->append($this->driver->stop(), self::UNCOVERED_FILES);
}
/**
* @throws ReflectionException
* @throws UnintentionallyCoveredCodeException
@ -648,28 +560,32 @@ final class CodeCoverage
}
/**
* @param list<string> $unintentionallyCoveredUnits
*
* @throws ReflectionException
*
* @return list<string>
*/
private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
{
$unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
sort($unintentionallyCoveredUnits);
$processed = [];
foreach ($unintentionallyCoveredUnits as $unintentionallyCoveredUnit) {
$tmp = explode('::', $unintentionallyCoveredUnit);
foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) {
$unit = explode('::', $unintentionallyCoveredUnits[$k]);
if (count($tmp) !== 2) {
$processed[] = $unintentionallyCoveredUnit;
if (count($unit) !== 2) {
continue;
}
try {
$class = new ReflectionClass($unit[0]);
$class = new ReflectionClass($tmp[0]);
foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) {
if ($class->isSubclassOf($parentClass)) {
unset($unintentionallyCoveredUnits[$k]);
break;
continue 2;
}
}
} catch (\ReflectionException $e) {
@ -679,9 +595,15 @@ final class CodeCoverage
$e
);
}
$processed[] = $tmp[0];
}
return array_values($unintentionallyCoveredUnits);
$processed = array_unique($processed);
sort($processed);
return $processed;
}
private function analyser(): FileAnalyser

@ -7,7 +7,7 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
namespace SebastianBergmann\CodeCoverage\Data;
use function array_key_exists;
use function array_keys;
@ -20,6 +20,10 @@ use SebastianBergmann\CodeCoverage\Driver\Driver;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type XdebugFunctionCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
*
* @psalm-type TestIdType = string
*/
final class ProcessedCodeCoverageData
{
@ -27,18 +31,33 @@ final class ProcessedCodeCoverageData
* Line coverage data.
* An array of filenames, each having an array of linenumbers, each executable line having an array of testcase ids.
*
* @var array
* @psalm-var array<string, array<int, null|list<TestIdType>>>
*/
private $lineCoverage = [];
private array $lineCoverage = [];
/**
* Function coverage data.
* Maintains base format of raw data (@see https://xdebug.org/docs/code_coverage), but each 'hit' entry is an array
* of testcase ids.
*
* @var array
* @psalm-var array<string, array<string, array{
* branches: array<int, array{
* op_start: int,
* op_end: int,
* line_start: int,
* line_end: int,
* hit: list<TestIdType>,
* out: array<int, int>,
* out_hit: array<int, int>,
* }>,
* paths: array<int, array{
* path: array<int, int>,
* hit: list<TestIdType>,
* }>,
* hit: list<TestIdType>
* }>>
*/
private $functionCoverage = [];
private array $functionCoverage = [];
public function initializeUnseenData(RawCodeCoverageData $rawData): void
{
@ -217,6 +236,8 @@ final class ProcessedCodeCoverageData
/**
* For a function we have never seen before, copy all data over and simply init the 'hit' array.
*
* @psalm-param XdebugFunctionCoverageType $functionData
*/
private function initPreviouslyUnseenFunction(string $file, string $functionName, array $functionData): void
{
@ -235,6 +256,8 @@ final class ProcessedCodeCoverageData
* For a function we have seen before, only copy over and init the 'hit' array for any unseen branches and paths.
* Techniques such as mocking and where the contents of a file are different vary during tests (e.g. compiling
* containers) mean that the functions inside a file cannot be relied upon to be static.
*
* @psalm-param XdebugFunctionCoverageType $functionData
*/
private function initPreviouslySeenFunction(string $file, string $functionName, array $functionData): void
{

@ -7,7 +7,7 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
namespace SebastianBergmann\CodeCoverage\Data;
use function array_diff;
use function array_diff_key;
@ -26,33 +26,39 @@ use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type XdebugFunctionsCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
* @psalm-import-type XdebugCodeCoverageWithoutPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
* @psalm-import-type XdebugCodeCoverageWithPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
*/
final class RawCodeCoverageData
{
/**
* @var array<string, array<int>>
*/
private static $emptyLineCache = [];
private static array $emptyLineCache = [];
/**
* @var array
*
* @see https://xdebug.org/docs/code_coverage for format
* @psalm-var XdebugCodeCoverageWithoutPathCoverageType
*/
private $lineCoverage;
private array $lineCoverage;
/**
* @var array
*
* @see https://xdebug.org/docs/code_coverage for format
* @psalm-var array<string, XdebugFunctionsCoverageType>
*/
private $functionCoverage;
private array $functionCoverage;
/**
* @psalm-param XdebugCodeCoverageWithoutPathCoverageType $rawCoverage
*/
public static function fromXdebugWithoutPathCoverage(array $rawCoverage): self
{
return new self($rawCoverage, []);
}
/**
* @psalm-param XdebugCodeCoverageWithPathCoverageType $rawCoverage
*/
public static function fromXdebugWithPathCoverage(array $rawCoverage): self
{
$lineCoverage = [];
@ -66,27 +72,6 @@ final class RawCodeCoverageData
return new self($lineCoverage, $functionCoverage);
}
public static function fromXdebugWithMixedCoverage(array $rawCoverage): self
{
$lineCoverage = [];
$functionCoverage = [];
foreach ($rawCoverage as $file => $fileCoverageData) {
if (!isset($fileCoverageData['functions'])) {
// Current file does not have functions, so line coverage
// is stored in $fileCoverageData, not in $fileCoverageData['lines']
$lineCoverage[$file] = $fileCoverageData;
continue;
}
$lineCoverage[$file] = $fileCoverageData['lines'];
$functionCoverage[$file] = $fileCoverageData['functions'];
}
return new self($lineCoverage, $functionCoverage);
}
public static function fromUncoveredFile(string $filename, FileAnalyser $analyser): self
{
$lineCoverage = [];
@ -98,6 +83,10 @@ final class RawCodeCoverageData
return new self([$filename => $lineCoverage], []);
}
/**
* @psalm-param XdebugCodeCoverageWithoutPathCoverageType $lineCoverage
* @psalm-param array<string, XdebugFunctionsCoverageType> $functionCoverage
*/
private function __construct(array $lineCoverage, array $functionCoverage)
{
$this->lineCoverage = $lineCoverage;
@ -111,11 +100,17 @@ final class RawCodeCoverageData
$this->lineCoverage = $this->functionCoverage = [];
}
/**
* @psalm-return XdebugCodeCoverageWithoutPathCoverageType
*/
public function lineCoverage(): array
{
return $this->lineCoverage;
}
/**
* @psalm-return array<string, XdebugFunctionsCoverageType>
*/
public function functionCoverage(): array
{
return $this->functionCoverage;

@ -11,11 +11,8 @@ namespace SebastianBergmann\CodeCoverage\Driver;
use function sprintf;
use SebastianBergmann\CodeCoverage\BranchAndPathCoverageNotSupportedException;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\DeadCodeDetectionNotSupportedException;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverAvailableException;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverWithPathCoverageSupportAvailableException;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
@ -55,45 +52,9 @@ abstract class Driver
*
* @see http://xdebug.org/docs/code_coverage
*/
public const BRANCH_HIT = 1;
/**
* @var bool
*/
private $collectBranchAndPathCoverage = false;
/**
* @var bool
*/
private $detectDeadCode = false;
/**
* @throws NoCodeCoverageDriverAvailableException
* @throws PcovNotAvailableException
* @throws PhpdbgNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
*
* @deprecated Use DriverSelector::forLineCoverage() instead
*/
public static function forLineCoverage(Filter $filter): self
{
return (new Selector)->forLineCoverage($filter);
}
/**
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
*
* @deprecated Use DriverSelector::forLineAndPathCoverage() instead
*/
public static function forLineAndPathCoverage(Filter $filter): self
{
return (new Selector)->forLineAndPathCoverage($filter);
}
public const BRANCH_HIT = 1;
private bool $collectBranchAndPathCoverage = false;
private bool $detectDeadCode = false;
public function canCollectBranchAndPathCoverage(): bool
{

@ -18,27 +18,22 @@ use function pcov\start;
use function pcov\stop;
use function pcov\waiting;
use function phpversion;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class PcovDriver extends Driver
{
/**
* @var Filter
*/
private $filter;
private readonly Filter $filter;
/**
* @throws PcovNotAvailableException
*/
public function __construct(Filter $filter)
{
if (!extension_loaded('pcov')) {
throw new PcovNotAvailableException;
}
$this->ensurePcovIsAvailable();
$this->filter = $filter;
}
@ -72,4 +67,14 @@ final class PcovDriver extends Driver
{
return 'PCOV ' . phpversion('pcov');
}
/**
* @throws PcovNotAvailableException
*/
private function ensurePcovIsAvailable(): void
{
if (!extension_loaded('pcov')) {
throw new PcovNotAvailableException;
}
}
}

@ -1,93 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use const PHP_SAPI;
use const PHP_VERSION;
use function array_diff;
use function array_keys;
use function array_merge;
use function get_included_files;
use function phpdbg_end_oplog;
use function phpdbg_get_executable;
use function phpdbg_start_oplog;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class PhpdbgDriver extends Driver
{
/**
* @throws PhpdbgNotAvailableException
*/
public function __construct()
{
if (PHP_SAPI !== 'phpdbg') {
throw new PhpdbgNotAvailableException;
}
}
public function start(): void
{
phpdbg_start_oplog();
}
public function stop(): RawCodeCoverageData
{
static $fetchedLines = [];
$dbgData = phpdbg_end_oplog();
if ($fetchedLines === []) {
$sourceLines = phpdbg_get_executable();
} else {
$newFiles = array_diff(get_included_files(), array_keys($fetchedLines));
$sourceLines = [];
if ($newFiles) {
$sourceLines = phpdbg_get_executable(['files' => $newFiles]);
}
}
foreach ($sourceLines as $file => $lines) {
foreach ($lines as $lineNo => $numExecuted) {
$sourceLines[$file][$lineNo] = self::LINE_NOT_EXECUTED;
}
}
$fetchedLines = array_merge($fetchedLines, $sourceLines);
return RawCodeCoverageData::fromXdebugWithoutPathCoverage(
$this->detectExecutedLines($fetchedLines, $dbgData)
);
}
public function nameAndVersion(): string
{
return 'PHPDBG ' . PHP_VERSION;
}
private function detectExecutedLines(array $sourceLines, array $dbgData): array
{
foreach ($dbgData as $file => $coveredLines) {
foreach ($coveredLines as $lineNo => $numExecuted) {
// phpdbg also reports $lineNo=0 when e.g. exceptions get thrown.
// make sure we only mark lines executed which are actually executable.
if (isset($sourceLines[$file][$lineNo])) {
$sourceLines[$file][$lineNo] = self::LINE_EXECUTED;
}
}
}
return $sourceLines;
}
}

@ -9,8 +9,6 @@
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use function phpversion;
use function version_compare;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverAvailableException;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverWithPathCoverageSupportAvailableException;
@ -21,29 +19,19 @@ final class Selector
/**
* @throws NoCodeCoverageDriverAvailableException
* @throws PcovNotAvailableException
* @throws PhpdbgNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
*/
public function forLineCoverage(Filter $filter): Driver
{
$runtime = new Runtime;
if ($runtime->hasPHPDBGCodeCoverage()) {
return new PhpdbgDriver;
}
if ($runtime->hasPCOV()) {
return new PcovDriver($filter);
}
if ($runtime->hasXdebug()) {
if (version_compare(phpversion('xdebug'), '3', '>=')) {
$driver = new Xdebug3Driver($filter);
} else {
$driver = new Xdebug2Driver($filter);
}
$driver = new XdebugDriver($filter);
$driver->enableDeadCodeDetection();
@ -55,18 +43,13 @@ final class Selector
/**
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
*/
public function forLineAndPathCoverage(Filter $filter): Driver
{
if ((new Runtime)->hasXdebug()) {
if (version_compare(phpversion('xdebug'), '3', '>=')) {
$driver = new Xdebug3Driver($filter);
} else {
$driver = new Xdebug2Driver($filter);
}
$driver = new XdebugDriver($filter);
$driver->enableDeadCodeDetection();
$driver->enableBranchAndPathCoverage();

@ -1,128 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use const XDEBUG_CC_BRANCH_CHECK;
use const XDEBUG_CC_DEAD_CODE;
use const XDEBUG_CC_UNUSED;
use const XDEBUG_FILTER_CODE_COVERAGE;
use const XDEBUG_PATH_INCLUDE;
use const XDEBUG_PATH_WHITELIST;
use function defined;
use function extension_loaded;
use function ini_get;
use function phpversion;
use function sprintf;
use function version_compare;
use function xdebug_get_code_coverage;
use function xdebug_set_filter;
use function xdebug_start_code_coverage;
use function xdebug_stop_code_coverage;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Xdebug2Driver extends Driver
{
/**
* @var bool
*/
private $pathCoverageIsMixedCoverage;
/**
* @throws WrongXdebugVersionException
* @throws Xdebug2NotEnabledException
* @throws XdebugNotAvailableException
*/
public function __construct(Filter $filter)
{
if (!extension_loaded('xdebug')) {
throw new XdebugNotAvailableException;
}
if (version_compare(phpversion('xdebug'), '3', '>=')) {
throw new WrongXdebugVersionException(
sprintf(
'This driver requires Xdebug 2 but version %s is loaded',
phpversion('xdebug')
)
);
}
if (!ini_get('xdebug.coverage_enable')) {
throw new Xdebug2NotEnabledException;
}
if (!$filter->isEmpty()) {
if (defined('XDEBUG_PATH_WHITELIST')) {
$listType = XDEBUG_PATH_WHITELIST;
} else {
$listType = XDEBUG_PATH_INCLUDE;
}
xdebug_set_filter(
XDEBUG_FILTER_CODE_COVERAGE,
$listType,
$filter->files()
);
}
$this->pathCoverageIsMixedCoverage = version_compare(phpversion('xdebug'), '2.9.6', '<');
}
public function canCollectBranchAndPathCoverage(): bool
{
return true;
}
public function canDetectDeadCode(): bool
{
return true;
}
public function start(): void
{
$flags = XDEBUG_CC_UNUSED;
if ($this->detectsDeadCode() || $this->collectsBranchAndPathCoverage()) {
$flags |= XDEBUG_CC_DEAD_CODE;
}
if ($this->collectsBranchAndPathCoverage()) {
$flags |= XDEBUG_CC_BRANCH_CHECK;
}
xdebug_start_code_coverage($flags);
}
public function stop(): RawCodeCoverageData
{
$data = xdebug_get_code_coverage();
xdebug_stop_code_coverage();
if ($this->collectsBranchAndPathCoverage()) {
if ($this->pathCoverageIsMixedCoverage) {
return RawCodeCoverageData::fromXdebugWithMixedCoverage($data);
}
return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
}
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
}
public function nameAndVersion(): string
{
return 'Xdebug ' . phpversion('xdebug');
}
}

@ -20,50 +20,56 @@ use function getenv;
use function in_array;
use function ini_get;
use function phpversion;
use function sprintf;
use function version_compare;
use function xdebug_get_code_coverage;
use function xdebug_info;
use function xdebug_set_filter;
use function xdebug_start_code_coverage;
use function xdebug_stop_code_coverage;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @see https://xdebug.org/docs/code_coverage#xdebug_get_code_coverage
*
* @psalm-type XdebugLinesCoverageType = array<int, int>
* @psalm-type XdebugBranchCoverageType = array{
* op_start: int,
* op_end: int,
* line_start: int,
* line_end: int,
* hit: int,
* out: array<int, int>,
* out_hit: array<int, int>,
* }
* @psalm-type XdebugPathCoverageType = array{
* path: array<int, int>,
* hit: int,
* }
* @psalm-type XdebugFunctionCoverageType = array{
* branches: array<int, XdebugBranchCoverageType>,
* paths: array<int, XdebugPathCoverageType>,
* }
* @psalm-type XdebugFunctionsCoverageType = array<string, XdebugFunctionCoverageType>
* @psalm-type XdebugPathAndBranchesCoverageType = array{
* lines: XdebugLinesCoverageType,
* functions: XdebugFunctionsCoverageType,
* }
* @psalm-type XdebugCodeCoverageWithoutPathCoverageType = array<string, XdebugLinesCoverageType>
* @psalm-type XdebugCodeCoverageWithPathCoverageType = array<string, XdebugPathAndBranchesCoverageType>
*/
final class Xdebug3Driver extends Driver
final class XdebugDriver extends Driver
{
/**
* @throws WrongXdebugVersionException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
*/
public function __construct(Filter $filter)
{
if (!extension_loaded('xdebug')) {
throw new XdebugNotAvailableException;
}
if (version_compare(phpversion('xdebug'), '3', '<')) {
throw new WrongXdebugVersionException(
sprintf(
'This driver requires Xdebug 3 but version %s is loaded',
phpversion('xdebug')
)
);
}
$mode = getenv('XDEBUG_MODE');
if ($mode === false || $mode === '') {
$mode = ini_get('xdebug.mode');
}
if ($mode === false ||
!in_array('coverage', explode(',', $mode), true)) {
throw new Xdebug3NotEnabledException;
}
$this->ensureXdebugIsAvailable();
$this->ensureXdebugCodeCoverageFeatureIsEnabled();
if (!$filter->isEmpty()) {
xdebug_set_filter(
@ -106,9 +112,11 @@ final class Xdebug3Driver extends Driver
xdebug_stop_code_coverage();
if ($this->collectsBranchAndPathCoverage()) {
/* @var XdebugCodeCoverageWithPathCoverageType $data */
return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
}
/* @var XdebugCodeCoverageWithoutPathCoverageType $data */
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
}
@ -116,4 +124,39 @@ final class Xdebug3Driver extends Driver
{
return 'Xdebug ' . phpversion('xdebug');
}
/**
* @throws XdebugNotAvailableException
*/
private function ensureXdebugIsAvailable(): void
{
if (!extension_loaded('xdebug')) {
throw new XdebugNotAvailableException;
}
}
/**
* @throws XdebugNotEnabledException
*/
private function ensureXdebugCodeCoverageFeatureIsEnabled(): void
{
if (version_compare(phpversion('xdebug'), '3.1', '>=')) {
if (!in_array('coverage', xdebug_info('mode'), true)) {
throw new XdebugNotEnabledException;
}
return;
}
$mode = getenv('XDEBUG_MODE');
if ($mode === false || $mode === '') {
$mode = ini_get('xdebug.mode');
}
if ($mode === false ||
!in_array('coverage', explode(',', $mode), true)) {
throw new XdebugNotEnabledException;
}
}
}

@ -7,11 +7,10 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
namespace SebastianBergmann\CodeCoverage;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Exception;
final class WrongXdebugVersionException extends RuntimeException implements Exception
final class FileCouldNotBeWrittenException extends RuntimeException implements Exception
{
}

@ -1,21 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Exception;
final class PhpdbgNotAvailableException extends RuntimeException implements Exception
{
public function __construct()
{
parent::__construct('The PHPDBG SAPI is not available');
}
}

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

Loading…
Cancel
Save