2023-10-07 23:46:31 +02:00
< ? php
/*
* This file is part of Part - DB ( https :// github . com / Part - DB / Part - DB - symfony ) .
*
* Copyright ( C ) 2019 - 2023 Jan Böhmer ( https :// github . com / jbtronics )
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*/
/*
* This file provide an interface with the Mouser API V2 ( also compatible with the V1 )
*
* Copyright ( C ) 2023 Pasquale D ' Orsi ( https :// github . com / pdo59 )
*
* TODO : Obtain an API keys with an US Mouser user ( currency $ ) and test the result of prices
*
*/
declare ( strict_types = 1 );
namespace App\Services\InfoProviderSystem\Providers ;
use App\Entity\Parts\ManufacturingStatus ;
use App\Services\InfoProviderSystem\DTOs\FileDTO ;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO ;
use App\Services\InfoProviderSystem\DTOs\PriceDTO ;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO ;
use Symfony\Contracts\HttpClient\HttpClientInterface ;
2023-10-08 00:00:10 +02:00
use Symfony\Contracts\HttpClient\ResponseInterface ;
2023-10-07 23:46:31 +02:00
class MouserProvider implements InfoProviderInterface
{
private const ENDPOINT_URL = 'https://api.mouser.com/api/v2/search' ;
public const DISTRIBUTOR_NAME = 'Mouser' ;
2023-10-08 00:15:57 +02:00
public function __construct (
private readonly HttpClientInterface $mouserClient ,
2023-10-07 23:46:31 +02:00
private readonly string $api_key ,
private readonly string $language ,
private readonly string $options ,
2023-10-08 00:15:57 +02:00
private readonly int $search_limit
) {
2023-10-07 23:46:31 +02:00
}
public function getProviderInfo () : array
{
return [
'name' => 'Mouser' ,
'description' => 'This provider uses the Mouser API to search for parts.' ,
'url' => 'https://www.mouser.com/' ,
'disabled_help' => 'Configure the API key in the PROVIDER_MOUSER_KEY environment variable to enable.'
];
}
public function getProviderKey () : string
{
2023-10-07 23:49:04 +02:00
return 'mouser' ;
2023-10-07 23:46:31 +02:00
}
public function isActive () : bool
{
2024-06-22 00:31:43 +02:00
return $this -> api_key !== '' ;
2023-10-07 23:46:31 +02:00
}
2023-10-08 00:00:10 +02:00
public function searchByKeyword ( string $keyword ) : array
2023-10-07 23:46:31 +02:00
{
/*
SearchByKeywordRequest description :
Search parts by keyword and return a maximum of 50 parts .
keyword * string
Used for keyword part search .
records integer ( $int32 )
Used to specify how many records the method should return .
startingRecord integer ( $int32 )
Indicates where in the total recordset the return set should begin .
From the startingRecord , the number of records specified will be returned up to the end of the recordset .
This is useful for paging through the complete recordset of parts matching keyword .
searchOptions string
Optional .
If not provided , the default is None .
Refers to options supported by the search engine .
Only one value at a time is supported .
Available options : None | Rohs | InStock | RohsAndInStock - can use string representations or integer IDs : 1 [ None ] | 2 [ Rohs ] | 4 [ InStock ] | 8 [ RohsAndInStock ] .
searchWithYourSignUpLanguage string
Optional .
If not provided , the default is false .
Used when searching for keywords in the language specified when you signed up for Search API .
Can use string representation : true .
{
" SearchByKeywordRequest " : {
" keyword " : " BC557 " ,
" records " : 0 ,
" startingRecord " : 0 ,
" searchOptions " : " " ,
" searchWithYourSignUpLanguage " : " "
}
}
*/
2023-10-08 00:18:25 +02:00
$response = $this -> mouserClient -> request ( 'POST' , self :: ENDPOINT_URL . " /keyword " , [
'query' => [
'apiKey' => $this -> api_key ,
],
2023-10-07 23:46:31 +02:00
'json' => [
'SearchByKeywordRequest' => [
2023-10-08 00:00:10 +02:00
'keyword' => $keyword ,
2023-10-07 23:46:31 +02:00
'records' => $this -> search_limit , //self::NUMBER_OF_RESULTS,
'startingRecord' => 0 ,
'searchOptions' => $this -> options ,
'searchWithYourSignUpLanguage' => $this -> language ,
]
],
]);
2023-10-08 00:00:10 +02:00
return $this -> responseToDTOArray ( $response );
2023-10-07 23:46:31 +02:00
}
2023-10-08 00:00:10 +02:00
public function getDetails ( string $id ) : PartDetailDTO
2023-10-07 23:46:31 +02:00
{
/*
SearchByPartRequest description :
Search parts by part number and return a maximum of 50 parts .
mouserPartNumber string
Used to search parts by the specific Mouser part number with a maximum input of 10 part numbers , separated by a pipe symbol for the search .
Each part number must be a minimum of 3 characters and a maximum of 40 characters . For example : 494 - JANTX2N2222A | 610 - 2 N2222 - TL | 637 - 2 N2222A
partSearchOptions string
Optional .
If not provided , the default is None . Refers to options supported by the search engine . Only one value at a time is supported .
The following values are valid : None | Exact - can use string representations or integer IDs : 1 [ None ] | 2 [ Exact ]
{
" SearchByPartRequest " : {
" mouserPartNumber " : " string " ,
" partSearchOptions " : " string "
}
}
*/
2023-10-08 00:18:25 +02:00
$response = $this -> mouserClient -> request ( 'POST' , self :: ENDPOINT_URL . " /partnumber " , [
'query' => [
'apiKey' => $this -> api_key ,
],
2023-10-07 23:46:31 +02:00
'json' => [
'SearchByPartRequest' => [
2023-10-08 00:00:10 +02:00
'mouserPartNumber' => $id ,
2023-10-07 23:46:31 +02:00
'partSearchOptions' => 2
]
],
]);
2023-10-08 00:00:10 +02:00
$tmp = $this -> responseToDTOArray ( $response );
//Ensure that we have exactly one result
if ( count ( $tmp ) === 0 ) {
2023-10-08 00:15:57 +02:00
throw new \RuntimeException ( 'No part found with ID ' . $id );
2023-10-08 00:00:10 +02:00
}
if ( count ( $tmp ) > 1 ) {
2024-06-22 23:37:50 +02:00
throw new \RuntimeException ( 'Multiple parts found with ID ' . $id . ' (' . count ( $tmp ) . ' found). This is basically a bug in Mousers API response. See issue #616.' );
2023-10-08 00:00:10 +02:00
}
return $tmp [ 0 ];
}
public function getCapabilities () : array
{
return [
ProviderCapabilities :: BASIC ,
ProviderCapabilities :: PICTURE ,
ProviderCapabilities :: DATASHEET ,
ProviderCapabilities :: PRICE ,
];
}
/**
2023-10-08 00:15:57 +02:00
* @ param ResponseInterface $response
2023-10-08 00:00:10 +02:00
* @ return PartDetailDTO []
*/
private function responseToDTOArray ( ResponseInterface $response ) : array
{
2023-10-07 23:46:31 +02:00
$arr = $response -> toArray ();
2023-10-08 00:00:10 +02:00
2023-10-07 23:46:31 +02:00
if ( isset ( $arr [ 'SearchResults' ])) {
$products = $arr [ 'SearchResults' ][ 'Parts' ] ? ? [];
} else {
throw new \RuntimeException ( 'Unknown response format' );
}
2024-03-04 22:18:35 +01:00
2023-10-07 23:46:31 +02:00
$result = [];
foreach ( $products as $product ) {
2024-03-09 21:34:05 +01:00
2024-06-22 23:37:50 +02:00
//Check if we have a valid product number. We assume that a product number, must have at least 4 characters
//Otherwise filter it out
if ( strlen ( $product [ 'MouserPartNumber' ]) < 4 ) {
continue ;
}
2024-03-09 21:34:05 +01:00
//Check if we have a mass field available
$mass = null ;
if ( isset ( $product [ 'UnitWeightKg' ][ 'UnitWeight' ])) {
$mass = ( float ) $product [ 'UnitWeightKg' ][ 'UnitWeight' ];
//The mass is given in kg, we want it in g
$mass *= 1000 ;
}
2023-10-07 23:46:31 +02:00
$result [] = new PartDetailDTO (
provider_key : $this -> getProviderKey (),
provider_id : $product [ 'MouserPartNumber' ],
name : $product [ 'ManufacturerPartNumber' ],
description : $product [ 'Description' ],
2023-10-08 00:15:57 +02:00
category : $product [ 'Category' ],
2023-10-07 23:46:31 +02:00
manufacturer : $product [ 'Manufacturer' ],
mpn : $product [ 'ManufacturerPartNumber' ],
preview_image_url : $product [ 'ImagePath' ],
2024-03-04 22:18:35 +01:00
manufacturing_status : $this -> releaseStatusCodeToManufacturingStatus (
$product [ 'LifecycleStatus' ] ? ? null ,
2024-03-09 21:34:05 +01:00
( int ) ( $product [ 'AvailabilityInStock' ] ? ? 0 )
2024-03-04 22:18:35 +01:00
),
2023-10-08 00:15:57 +02:00
provider_url : $product [ 'ProductDetailUrl' ],
datasheets : $this -> parseDataSheets ( $product [ 'DataSheetUrl' ] ? ? null ,
$product [ 'MouserPartNumber' ] ? ? null ),
vendor_infos : $this -> pricingToDTOs ( $product [ 'PriceBreaks' ] ? ? [], $product [ 'MouserPartNumber' ],
$product [ 'ProductDetailUrl' ]),
2024-03-09 21:34:05 +01:00
mass : $mass ,
2023-10-07 23:46:31 +02:00
);
}
return $result ;
}
2023-10-08 00:15:57 +02:00
private function parseDataSheets ( ? string $sheetUrl , ? string $sheetName ) : ? array
2023-10-07 23:46:31 +02:00
{
2024-06-22 00:31:43 +02:00
if ( $sheetUrl === null || $sheetUrl === '' || $sheetUrl === '0' ) {
2023-10-07 23:46:31 +02:00
return null ;
}
$result = [];
$result [] = new FileDTO ( url : $sheetUrl , name : $sheetName );
return $result ;
}
/*
* Mouser API price is a string in the form " n[.,]nnn[.,] currency "
* then this convert it to a number
2024-03-04 22:38:15 +01:00
* Austria has a format like " € 2,10 "
2023-10-07 23:46:31 +02:00
*/
2023-10-08 00:15:57 +02:00
private function priceStrToFloat ( $val ) : float
{
2024-03-04 22:38:15 +01:00
//Remove any character that is not a number, dot or comma (like currency symbols)
$val = preg_replace ( '/[^0-9.,]/' , '' , $val );
//Trim the string
$val = trim ( $val );
//Convert commas to dots
2023-10-08 00:15:57 +02:00
$val = str_replace ( " , " , " . " , $val );
2024-03-04 22:38:15 +01:00
//Remove any dot that is not the last one (to avoid problems with thousands separators)
2023-10-07 23:46:31 +02:00
$val = preg_replace ( '/\.(?=.*\.)/' , '' , $val );
2023-10-08 00:00:10 +02:00
return ( float ) $val ;
2023-10-07 23:46:31 +02:00
}
/**
* Converts the pricing ( StandardPricing field ) from the Mouser API to an array of PurchaseInfoDTOs
* @ param array $price_breaks
* @ param string $order_number
* @ param string $product_url
* @ return PurchaseInfoDTO []
*/
private function pricingToDTOs ( array $price_breaks , string $order_number , string $product_url ) : array
{
$prices = [];
foreach ( $price_breaks as $price_break ) {
2023-10-08 00:15:57 +02:00
$number = $this -> priceStrToFloat ( $price_break [ 'Price' ]);
$prices [] = new PriceDTO (
minimum_discount_amount : $price_break [ 'Quantity' ],
price : ( string ) $number ,
currency_iso_code : $price_break [ 'Currency' ]
);
2023-10-07 23:46:31 +02:00
}
return [
2023-10-08 00:15:57 +02:00
new PurchaseInfoDTO ( distributor_name : self :: DISTRIBUTOR_NAME , order_number : $order_number , prices : $prices ,
product_url : $product_url )
2023-10-07 23:46:31 +02:00
];
}
/* Converts the product status from the MOUSER API to the manufacturing status used in Part - DB :
Factory Special Order - Ordine speciale in fabbrica
Not Recommended for New Designs - Non raccomandato per nuovi progetti
New Product - Nuovo prodotto
End of Life - Fine vita
- vuoto - - Attivo
TODO : Probably need to review the values of field Lifecyclestatus
*/
2024-03-04 22:18:35 +01:00
/**
* Converts the lifecycle status from the Mouser API to a ManufacturingStatus
* @ param string | null $productStatus The lifecycle status from the Mouser API
* @ param int $availableInStock The number of parts available in stock
* @ return ManufacturingStatus | null
*/
private function releaseStatusCodeToManufacturingStatus ( ? string $productStatus , int $availableInStock = 0 ) : ? ManufacturingStatus
2023-10-07 23:46:31 +02:00
{
2024-03-04 22:18:35 +01:00
$tmp = match ( $productStatus ) {
2023-10-07 23:46:31 +02:00
null => null ,
" New Product " => ManufacturingStatus :: ANNOUNCED ,
" Not Recommended for New Designs " => ManufacturingStatus :: NRFND ,
2024-03-03 20:33:24 +01:00
" Factory Special Order " , " Obsolete " => ManufacturingStatus :: DISCONTINUED ,
2023-10-07 23:46:31 +02:00
" End of Life " => ManufacturingStatus :: EOL ,
default => ManufacturingStatus :: ACTIVE ,
};
2024-03-04 22:18:35 +01:00
//If the part would be assumed to be announced, check if it is in stock, then it is active
if ( $tmp === ManufacturingStatus :: ANNOUNCED && $availableInStock > 0 ) {
2024-03-04 22:42:24 +01:00
$tmp = ManufacturingStatus :: ACTIVE ;
2024-03-04 22:18:35 +01:00
}
2024-03-04 22:42:24 +01:00
return $tmp ;
2023-10-07 23:46:31 +02:00
}
}