filipac / withings-sdk
Laravel package for Withings API integration
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0
- pestphp/pest: *
- pestphp/pest-plugin-laravel: *
- phpunit/phpunit: ^10.0
README
Withings SDK for Laravel
A comprehensive Laravel package for integrating with the Withings API.
Features
- Complete Withings API Coverage: All endpoints including measures, heart, sleep, user, notifications, and dropshipment
- π― Type-Safe PHP 8.1+ Enums: Eliminates magic strings with dedicated enums for API actions, measurement types, categories, and fields
- π¦ Laravel Collections: Fluent data manipulation with Collection methods instead of plain arrays
- π§ Type-Safe Parameter DTOs: Constructor property promotion with automatic enum conversion
- π OAuth2 Authentication Flow: Full authentication support with token refresh
- βοΈ Laravel Service Provider Integration: Auto-discovery and configuration publishing
- π Facade Support: Clean, Laravel-style API access with full IDE completion
- β‘ Comprehensive Error Handling: Detailed Withings API error mapping
- π‘οΈ Readonly DTOs: Immutable data objects with automatic type conversion
- πͺ Enhanced Measurement Types: Support for all Withings measurement types with enum-based categorization
Installation
Add the package to your Laravel project:
composer require filipac/withings-sdk
Configuration
Publish the configuration file:
php artisan vendor:publish --tag=withings-config
Add your Withings API credentials to your .env
file:
WITHINGS_CLIENT_ID=your_client_id WITHINGS_CLIENT_SECRET=your_client_secret WITHINGS_ACCESS_TOKEN=your_access_token WITHINGS_REFRESH_TOKEN=your_refresh_token
Usage
Basic Usage
use Filipac\Withings\Client\WithingsClient; use Filipac\Withings\DataTransferObjects\Parameters\GetMeasuresParams; use Filipac\Withings\Enums\MeasurementType; // Create client with tokens $client = new WithingsClient( accessToken: 'your_access_token', refreshToken: 'your_refresh_token', clientId: 'your_client_id', clientSecret: 'your_client_secret' ); // π― Type-safe enums eliminate magic strings $params = new GetMeasuresParams( meastype: MeasurementType::WEIGHT, // Enum instead of magic number startdate: strtotime('2024-01-01'), enddate: strtotime('2024-12-31') ); // Or use convenience factory methods $params = GetMeasuresParams::forWeight( strtotime('2024-01-01'), strtotime('2024-12-31') ); $measurementData = $client->measures()->getMeasures($params); // π¦ Get Laravel Collections instead of arrays $weightMeasurements = $measurementData->getWeightMeasurements(); // Collection $latestWeight = $measurementData->getLatestMeasurement(MeasurementType::WEIGHT); // Single measurement // Individual measurements are automatically converted to enums $measurement = $weightMeasurements->first(); echo $measurement->type->getName(); // "Weight" (from enum) echo $measurement->type->getUnit(); // "kg" (from enum) echo $measurement->category?->getName(); // "Real measurement" (from enum)
Using the Facade (with Full IDE Completion)
use Filipac\Withings\Facades\Withings; use Filipac\Withings\Enums\NotificationApplication; use Filipac\Withings\Enums\MeasurementType; // Full IDE autocompletion for all methods $measurements = Withings::measures()->getWeight(); // Type-safe enum parameters with IDE hints Withings::notifications()->subscribe( 'https://your-webhook-url.com', NotificationApplication::WEIGHT, 'My webhook comment' ); // Access all services with complete method documentation $heartData = Withings::heart()->getHeartRate(); $sleepSummary = Withings::sleep()->getSummary(); $userInfo = Withings::user()->getInfo();
The facade provides complete IDE autocompletion with:
- β Method signatures with parameter types and return types
- β Enum suggestions for type-safe parameters
- β Documentation with usage examples
- β Service method chaining with full completion at each step
π― Type-Safe Enums
All magic strings and numbers are replaced with type-safe enums:
use Filipac\Withings\Enums\{MeasurementType, MeasurementCategory, NotificationApplication, SleepField}; // β Type-safe measurement types $weight = MeasurementType::WEIGHT; $bodyFat = MeasurementType::FAT_RATIO; $heartRate = MeasurementType::HEART_RATE; // β Type-safe categories $realMeasurement = MeasurementCategory::REAL; $manualEntry = MeasurementCategory::MANUAL; // β Type-safe notification types $weightNotifications = NotificationApplication::WEIGHT; // β Type-safe sleep fields $sleepFields = [SleepField::DEEP_SLEEP_DURATION, SleepField::REM_SLEEP_DURATION]; // Get enum information echo MeasurementType::WEIGHT->getName(); // "Weight" echo MeasurementType::WEIGHT->getUnit(); // "kg" echo MeasurementCategory::REAL->getDescription(); // "Real measurement taken by device"
π¦ Laravel Collections
All data methods return Laravel Collections for powerful data manipulation:
$measurements = Withings::measures()->getWeight()->getMeasurements(); // Collection<Measurement> // β Fluent filtering and chaining $recentWeights = $measurements ->filter(fn($m) => $m->type === MeasurementType::WEIGHT) ->sortByDesc('date') ->take(10) ->map(fn($m) => [ 'weight' => $m->getRealValue(), 'date' => $m->getDateTime()->format('Y-m-d'), 'category' => $m->category?->getName() ]); // β Powerful aggregations $averageWeight = $measurements->avg('value') * pow(10, -3); // Convert to kg $weightTrend = $measurements->pluck('value', 'date'); // β Activity data with Collections $activities = Withings::measures()->getActivity()->getActivities(); $totalSteps = $activities->sum('steps'); $averageDailySteps = $activities->groupBy('date')->map->sum('steps')->avg(); // β Collection-specific methods $activitiesByDate = $activities->getActivitiesGroupedByDate(); $weeklyAverage = $activities->getAverageDailySteps();
OAuth2 Flow
// Generate authorization URL $authUrl = $client->oauth2()->getAuthorizationUrl( redirectUri: 'https://your-app.com/callback', state: 'optional-state', scopes: ['user.info', 'user.metrics'] ); // Exchange code for tokens $tokens = $client->oauth2()->getAccessToken($code, $redirectUri); // Refresh tokens $refreshedTokens = $client->oauth2()->refreshToken();
Available Services
Measure Service
getMeasures(?GetMeasuresParams $params)
- Get body measurements with type-safe parametersgetWeight(?int $startdate, ?int $enddate, ?int $lastupdate)
- Get weight measurementsgetHeight(?int $startdate, ?int $enddate)
- Get height measurementsgetBodyComposition(?int $startdate, ?int $enddate)
- Get body composition datagetVitalSigns(?int $startdate, ?int $enddate)
- Get blood pressure and heart rategetTemperature(?int $startdate, ?int $enddate)
- Get temperature measurementsgetActivityMeasures(?GetActivityParams $params)
- Get activity datagetWorkouts(?GetWorkoutsParams $params)
- Get workout datagetIntradayActivity(?GetIntradayActivityParams $params)
- Get detailed activity data
Heart Service
getHeartRate(?GetHeartParams $params)
- Get heart rate data with parameterslist(?int $startdate, ?int $enddate)
- List heart rate measurementsgetEcg(int $signalid)
- Get ECG data by signal IDgetDetailed(?int $startdate, ?int $enddate, ?int $offset)
- Get detailed heart data
Sleep Service
getSummary(?GetSleepParams $params)
- Get sleep summary with parametersget(?GetSleepParams $params)
- Get detailed sleep data with parametersgetSummaryByDateRange(string $startdateymd, string $enddateymd)
- Get sleep summary for date range (Y-m-d format)getByDateRange(int $startdate, int $enddate)
- Get detailed sleep data for date range (Unix timestamps)getSleepWithFields(array $dataFields, ?int $startdate, ?int $enddate)
- Get specific sleep fields
User Service
getInfo()
- Get user informationgetDevices()
- Get user devicesgetGoals()
- Get user goalsgetTimezone()
- Get user's timezonegetPreferences()
- Get user preferences
Notification Service
subscribe(string $callbackurl, ?string $comment, int $appli)
- Subscribe to notificationsrevoke(string $callbackurl, int $appli)
- Revoke notificationsget(?int $appli)
- Get notification informationlist()
- List all notifications
Dropshipment Service
createOrder(array $orderData)
- Create a new dropshipment orderupdateOrder(string $orderId, array $updateData)
- Update existing ordergetOrder(string $orderId)
- Get order detailslistOrders(array $filters)
- List orders with filterscancelOrder(string $orderId)
- Cancel an ordergetProducts()
- Get available productsgetShippingMethods(array $shippingData)
- Get shipping options
π‘οΈ Enhanced Data Transfer Objects
The package includes immutable DTOs with automatic type conversion and Collection support:
use Filipac\Withings\Enums\MeasurementType; use Filipac\Withings\DataTransferObjects\Parameters\GetMeasuresParams; // π§ Type-safe parameter creation with factory methods $params = GetMeasuresParams::forBodyComposition( startdate: strtotime('-30 days'), enddate: time() ); $measurementData = $client->measures()->getMeasures($params); // π¦ Get Collection instead of array $measurements = $measurementData->getMeasurements(); // Collection<Measurement> // π― Automatic enum conversion - no more magic numbers! $measurement = $measurements->first(); echo $measurement->type; // MeasurementType::WEIGHT (enum) echo $measurement->category; // MeasurementCategory::REAL (enum) // β Rich enum methods echo $measurement->type->getName(); // "Weight" echo $measurement->type->getUnit(); // "kg" echo $measurement->category?->getDescription(); // "Real measurement taken by device" // π Collection-powered analysis $bodyCompositionData = $measurements ->filter(fn($m) => $m->isBodyComposition()) ->sortByDesc('date') ->groupBy(fn($m) => $m->getDateTime()->format('Y-m')) ->map(fn($monthlyData) => [ 'count' => $monthlyData->count(), 'avg_value' => $monthlyData->avg('value'), 'types' => $monthlyData->pluck('type')->unique()->map->getName() ]); // π― Type-safe filtering with enums $weightMeasurements = $measurementData->getMeasurementsByType(MeasurementType::WEIGHT); $latestWeight = $measurementData->getLatestMeasurement(MeasurementType::WEIGHT);
β‘ Error Handling
use Filipac\Withings\Exceptions\WithingsException; try { $measurements = Withings::measures()->getWeight(); // Using facade } catch (WithingsException $e) { echo "Withings API Error: " . $e->getMessage(); echo "Status Code: " . $e->getCode(); }
π What Makes This Package Special
β Complete Type Safety
- No magic strings or numbers - Everything uses PHP 8.1+ enums
- Automatic type conversion - Raw API responses become typed objects
- Full IDE completion - Autocomplete for all methods, parameters, and enum values
β Laravel-First Design
- Collections everywhere - Powerful data manipulation with Collection methods
- Service provider integration - Auto-discovery and configuration publishing
- Facade support - Clean, Laravel-style static access
- Follows Laravel conventions - Feels native to Laravel applications
β Developer Experience
- Rich factory methods -
GetMeasuresParams::forWeight()
,::forBodyComposition()
- Fluent data analysis - Chain filters, sorts, and aggregations
- Comprehensive error handling - Detailed Withings API error mapping
- Immutable DTOs - Readonly objects prevent accidental mutations
β Production Ready
- Complete API coverage - All Withings endpoints supported
- Robust authentication - Full OAuth2 flow with token refresh
- Extensible design - Easy to add custom methods and functionality
- Well documented - Comprehensive examples and use cases
Testing
This package uses Pest for testing. To run the tests:
# Run all tests composer test # Run tests with coverage composer test-coverage # Run specific test file ./vendor/bin/pest tests/Unit/WithingsClientTest.php # Run tests in watch mode (if pest-plugin-watch is installed) ./vendor/bin/pest --watch
Test Structure
- Unit Tests (
tests/Unit/
): Test individual classes and methods in isolation - Feature Tests (
tests/Feature/
): Test integration with Laravel framework and service provider - Test Helpers (
tests/Pest.php
): Custom expectations and helper functions
Example test:
it('can create a withings client instance', function () { $client = new WithingsClient( clientId: 'test-client-id', clientSecret: 'test-client-secret' ); expect($client)->toBeWithingsClient(); });
Custom Expectations & Test Helpers
The package includes custom Pest expectations and testing utilities:
toBeWithingsClient()
- Verify instance is a WithingsClientmockWithingsResponse()
- Helper to create consistent mock API responsescreateMockClient()
- Helper to create HTTP-mocked WithingsClient instances
Test Coverage
The package includes 42+ comprehensive tests (85+ assertions) covering:
- β Unit Tests - All services, DTOs, enums, and client functionality
- β Feature Tests - Laravel integration and service provider
- β Integration Tests - Complete OAuth workflows and API interactions
- β Performance Tests - Large dataset handling and memory efficiency
- β Edge Case Tests - Error conditions and boundary scenarios
- β Architecture Tests - Code quality and structural integrity
Test Categories:
- Client & Core Functionality: 7 tests
- Service Layer (Measures, User, Notifications, Heart): 17 tests
- Data Transfer Objects: 8 tests
- Enums & Types: 4 tests
- Laravel Integration: 4 tests
- Performance & Memory: 6 tests
Testing Infrastructure
HTTP Client Mocking: The WithingsClient
includes public setHttpClient()
and getHttpClient()
methods specifically designed for testing, allowing easy injection of mock HTTP clients without complex reflection.
Requirements
- PHP 8.2+
- Laravel 10.0+
- Guzzle 7.0+
License
MIT