├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── composer.json ├── LICENSE ├── src ├── Xrm │ ├── Query │ │ ├── QueryBase.php │ │ ├── FetchExpression.php │ │ ├── PagingInfo.php │ │ ├── OrderType.php │ │ └── QueryByAttribute.php │ ├── Metadata │ │ ├── ConstantsBase.php │ │ ├── StateAttributeMetadata.php │ │ ├── StatusAttributeMetadata.php │ │ ├── MetadataBase.php │ │ ├── ManagedPropertyAttributeMetadata.php │ │ ├── BigIntAttributeMetadata.php │ │ ├── EntityNameAttributeMetadata.php │ │ ├── UniqueIdentifierAttributeMetadata.php │ │ ├── LookupFormat.php │ │ ├── EnumAttributeMetadata.php │ │ ├── DateTimeFormat.php │ │ ├── OptionSetMetadata.php │ │ ├── FileAttributeMetadata.php │ │ ├── LookupAttributeMetadata.php │ │ ├── OptionSetType.php │ │ ├── DateTimeBehavior.php │ │ ├── AssociatedMenuBehavior.php │ │ ├── IntegerFormat.php │ │ ├── MultiSelectPicklistAttributeMetadata.php │ │ ├── PicklistAttributeMetadata.php │ │ ├── RelationshipType.php │ │ ├── BooleanOptionSetMetadata.php │ │ ├── ImeMode.php │ │ ├── AttributeRequiredLevel.php │ │ ├── DoubleAttributeMetadata.php │ │ ├── AssociatedMenuGroup.php │ │ ├── MemoAttributeMetadata.php │ │ ├── StringFormat.php │ │ ├── ImageAttributeMetadata.php │ │ ├── AssociatedMenuConfiguration.php │ │ ├── IntegerAttributeMetadata.php │ │ ├── StringFormatName.php │ │ ├── SecurityPrivilegeMetadata.php │ │ ├── CascadeConfiguration.php │ │ ├── DecimalAttributeMetadata.php │ │ ├── OwnershipTypes.php │ │ ├── PrivilegeType.php │ │ ├── SecurityTypes.php │ │ ├── OptionMetadata.php │ │ ├── DateTimeAttributeMetadata.php │ │ ├── RelationshipMetadataBase.php │ │ ├── BooleanAttributeMetadata.php │ │ ├── StringAttributeMetadata.php │ │ ├── OptionSetMetadataBase.php │ │ ├── OneToManyRelationshipMetadata.php │ │ ├── MoneyAttributeMetadata.php │ │ ├── CascadeType.php │ │ ├── EntityKeyMetadata.php │ │ ├── ManyToManyRelationshipMetadata.php │ │ ├── AttributeTypeCode.php │ │ └── AttributeTypeDisplayName.php │ ├── Label.php │ ├── ManagedProperty.php │ ├── EntityRole.php │ ├── Relationship.php │ ├── EntityKeyIndexStatus.php │ ├── EntityReference.php │ ├── LocalizedLabel.php │ ├── ColumnSet.php │ ├── EntityLikeTrait.php │ ├── EntityCollection.php │ ├── AttributeState.php │ └── IOrganizationService.php ├── WebAPI │ ├── OrganizationException.php │ ├── OData │ │ ├── TransportException.php │ │ ├── AuthenticationException.php │ │ ├── AuthMiddlewareInterface.php │ │ ├── EntityNotSupportedException.php │ │ ├── MiddlewareInterface.php │ │ ├── Annotation.php │ │ ├── ListResponse.php │ │ ├── Exception.php │ │ ├── OnlineSettings.php │ │ ├── TransferOptionMiddleware.php │ │ ├── Token.php │ │ ├── Settings.php │ │ └── ODataException.php │ ├── ToolkitException.php │ ├── Exception.php │ ├── MetadataDeserializer.php │ └── ClientFactory.php ├── Enum │ ├── FlaggedEnum.php │ └── ChoiceEnum.php └── Cache │ └── NullCacheItem.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea/ 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexacrm/dynamics-webapi-toolkit", 3 | "description": "Web API toolkit for Microsoft Dynamics 365 and Dynamics CRM", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "AlexaCRM", 9 | "email": "info@alexacrm.com" 10 | } 11 | ], 12 | "minimum-stability": "dev", 13 | "require": { 14 | "php": ">=7.4", 15 | "psr/log": "^1.0", 16 | "psr/cache": "^1.0", 17 | "elao/enum": "^1.6", 18 | "alexacrm/strong-serializer": "^2.0", 19 | "guzzlehttp/guzzle": "^7.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "AlexaCRM\\": "src/" 24 | } 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^9.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 AlexaCRM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Xrm/Query/QueryBase.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/UniqueIdentifierAttributeMetadata.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Xrm/Label.php: -------------------------------------------------------------------------------- 1 | Query = $query; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/DateTimeFormat.php: -------------------------------------------------------------------------------- 1 | Options = $optionMetadataCollection; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Xrm/EntityRole.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/FileAttributeMetadata.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/LookupAttributeMetadata.php: -------------------------------------------------------------------------------- 1 | Format = $format; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Xrm/Query/PagingInfo.php: -------------------------------------------------------------------------------- 1 | getConstants(); 42 | foreach (static::values() as $value) { 43 | $constantName = array_search($value, $constants, true); 44 | self::$readables[$enumType][$value] = $constantName; 45 | } 46 | } 47 | 48 | return self::$readables[$enumType]; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/MultiSelectPicklistAttributeMetadata.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/PicklistAttributeMetadata.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/WebAPI/OData/ListResponse.php: -------------------------------------------------------------------------------- 1 | TrueOption = $trueOption; 50 | $this->FalseOption = $falseOption; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/ImeMode.php: -------------------------------------------------------------------------------- 1 | message = $message; 44 | $this->innerException = $inner; 45 | } 46 | 47 | /** 48 | * Returns the underlying exception with relevant information about the error. 49 | * 50 | * @return \Exception 51 | */ 52 | public function getInnerException() { 53 | return $this->innerException; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/WebAPI/OData/Exception.php: -------------------------------------------------------------------------------- 1 | message = $message; 44 | $this->innerException = $inner; 45 | } 46 | 47 | /** 48 | * Returns the underlying exception with relevant information about the error. 49 | * 50 | * @return \Exception 51 | */ 52 | public function getInnerException() { 53 | return $this->innerException; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/WebAPI/MetadataDeserializer.php: -------------------------------------------------------------------------------- 1 | getClassName( $data ); 46 | 47 | // Create enums separately. 48 | if ( is_subclass_of( $className, EnumInterface::class ) ) { 49 | return $className::$data(); 50 | } 51 | 52 | return parent::toStrong( $data, $type ); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/AttributeRequiredLevel.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/AssociatedMenuGroup.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/Xrm/EntityKeyIndexStatus.php: -------------------------------------------------------------------------------- 1 | constructOverloaded( $entityName, $entityId, $keyValue ); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/StringFormat.php: -------------------------------------------------------------------------------- 1 | instanceURI, '/' ) . '/api/data/v' . $this->apiVersion . '/'; 61 | } 62 | 63 | /** 64 | * Check if authentication is certificate based or not 65 | * @return bool 66 | */ 67 | public function isCertificateBasedAuth(): bool { 68 | return empty($this->applicationSecret) && !empty($this->certificatePath); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/ImageAttributeMetadata.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/Xrm/LocalizedLabel.php: -------------------------------------------------------------------------------- 1 | Label = $label; 65 | $this->LanguageCode = $languageCode; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/AssociatedMenuConfiguration.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/StringFormatName.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/OwnershipTypes.php: -------------------------------------------------------------------------------- 1 | Label = $label; 74 | 75 | if ( $value !== null ) { 76 | $this->Value = $value; 77 | } 78 | } elseif ( is_int( $label ) ) { 79 | $this->Value = $label; 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/WebAPI/OData/TransferOptionMiddleware.php: -------------------------------------------------------------------------------- 1 | optionName = $optionName; 49 | $this->optionValue = $optionValue; 50 | } 51 | 52 | /** 53 | * Returns a Guzzle-compliant middleware. 54 | * Middleware should only operate with request options included in the transfer options list. 55 | * 56 | * @return callable 57 | * 58 | * @see http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html#creating-a-handler 59 | */ 60 | public function getMiddleware(): callable { 61 | $self = $this; 62 | 63 | return static function ( callable $handler ) use ( $self ) { 64 | return static function ( RequestInterface $request, $options ) use ( $handler, $self ) { 65 | $options[ $self->optionName ] = $self->optionValue; 66 | 67 | return $handler( $request, $options ); 68 | }; 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/DateTimeAttributeMetadata.php: -------------------------------------------------------------------------------- 1 | AllColumns = true; 51 | 52 | return; 53 | } 54 | 55 | $this->Columns = $columns; 56 | } 57 | 58 | /** 59 | * Adds an attribute to the column set. 60 | * 61 | * @param string $column 62 | */ 63 | public function AddColumn( string $column ): void { 64 | if ( in_array( $column, $this->Columns, true ) ) { 65 | return; 66 | } 67 | 68 | $this->Columns[] = $column; 69 | $this->Columns = array_unique( $this->Columns ); 70 | } 71 | 72 | /** 73 | * Adds multiple columns to the column set. 74 | * 75 | * @param string[] $columns 76 | */ 77 | public function AddColumns( array $columns ): void { 78 | $this->Columns = array_merge( $this->Columns, $columns ); 79 | $this->Columns = array_unique( $this->Columns ); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/BooleanAttributeMetadata.php: -------------------------------------------------------------------------------- 1 | OptionSet = $schemaName; 67 | } elseif ( is_string( $schemaName ) ) { 68 | $this->SchemaName = $schemaName; 69 | 70 | if ( $optionSet instanceof BooleanOptionSetMetadata ) { 71 | $this->OptionSet = $optionSet; 72 | } 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/StringAttributeMetadata.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/Xrm/EntityLikeTrait.php: -------------------------------------------------------------------------------- 1 | LogicalName = $entityName; 56 | 57 | if ( $entityId === null && $keyValue === null ) { 58 | return; 59 | } 60 | 61 | if ( $entityId instanceof KeyAttributeCollection ) { 62 | $keyAttributes = $entityId; 63 | $this->KeyAttributes = $keyAttributes; 64 | 65 | return; 66 | } 67 | 68 | if ( is_string( $entityId ) && $keyValue === null ) { 69 | $this->Id = $entityId; 70 | 71 | return; 72 | } 73 | 74 | $this->KeyAttributes = new KeyAttributeCollection(); 75 | $keyName = $entityId; 76 | $this->KeyAttributes->Add( $keyName, $keyValue ); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/OptionSetMetadataBase.php: -------------------------------------------------------------------------------- 1 | SchemaName = $schemaName; 96 | } 97 | 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/WebAPI/ClientFactory.php: -------------------------------------------------------------------------------- 1 | instanceURI = $instanceURI; 53 | $settings->applicationID = $applicationID; 54 | $settings->applicationSecret = $applicationSecret; 55 | 56 | if ( isset ( $services['logger'] ) && $services['logger'] instanceof LoggerInterface ) { 57 | $settings->setLogger( $services['logger'] ); 58 | } 59 | if ( isset ( $services['cachePool'] ) && $services['cachePool'] instanceof CacheItemPoolInterface ) { 60 | $settings->cachePool = $services['cachePool']; 61 | } 62 | 63 | $middleware = new OnlineAuthMiddleware( $settings ); 64 | $odataClient = new OData\Client( $settings, $middleware ); 65 | 66 | return new Client( $odataClient ); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/CascadeType.php: -------------------------------------------------------------------------------- 1 | Entities ); 68 | } 69 | 70 | /** 71 | * Move forward to next element. 72 | */ 73 | public function next(): void { 74 | next( $this->Entities ); 75 | } 76 | 77 | /** 78 | * Return the key of the current element. 79 | * 80 | * @return mixed Scalar on success, or null on failure. 81 | */ 82 | public function key() { 83 | return key( $this->Entities ); 84 | } 85 | 86 | /** 87 | * Checks if current position is valid. 88 | * 89 | * @return boolean Returns true on success or false on failure. 90 | */ 91 | public function valid(): bool { 92 | return isset( $this->Entities[ $this->key() ] ); 93 | } 94 | 95 | /** 96 | * Rewind the Iterator to the first element. 97 | */ 98 | public function rewind(): void { 99 | reset( $this->Entities ); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/Xrm/Query/QueryByAttribute.php: -------------------------------------------------------------------------------- 1 | value used in the query. 33 | */ 34 | public array $Attributes = []; 35 | 36 | /** 37 | * The column set selected for the query. 38 | */ 39 | public ?ColumnSet $ColumnSet = null; 40 | 41 | /** 42 | * Name of the entity to query. 43 | */ 44 | public ?string $EntityName = null; 45 | 46 | /** 47 | * Specifies the order in which the entity instances are returned from the query. 48 | * 49 | * @var OrderType[] 50 | */ 51 | public array $Orders = []; 52 | 53 | /** 54 | * The number of pages and the number of entity instances per page returned from the query. 55 | */ 56 | public ?PagingInfo $PageInfo = null; 57 | 58 | /** 59 | * The number of rows to be returned. 60 | */ 61 | public ?int $TopCount = null; 62 | 63 | /** 64 | * QueryByAttribute constructor. 65 | * 66 | * @param string|null $entityName 67 | */ 68 | public function __construct( string $entityName = null ) { 69 | $this->EntityName = $entityName; 70 | } 71 | 72 | /** 73 | * Adds an attribute value to the attributes collection. 74 | * 75 | * @param string $attributeName The logical name of the attribute. 76 | * @param mixed $value The attribute value. 77 | */ 78 | public function AddAttributeValue( string $attributeName, $value ): void { 79 | $this->Attributes[$attributeName] = $value; 80 | } 81 | 82 | /** 83 | * Adds an order to the orders collection. 84 | * 85 | * @param string $attributeName The logical name of the attribute. 86 | * @param OrderType $orderType The order for that attribute. 87 | */ 88 | public function AddOrder( string $attributeName, OrderType $orderType ): void { 89 | $this->Orders[$attributeName] = $orderType; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/WebAPI/OData/Token.php: -------------------------------------------------------------------------------- 1 | type = $tokenArray['token_type'] ?? null; 66 | $token->expiresIn = isset( $tokenArray['expires_in'] )? (int)$tokenArray['expires_in'] : null; 67 | $token->expiresOn = isset( $tokenArray['expires_on'] )? (int)$tokenArray['expires_on'] : null; 68 | $token->notBefore = isset( $tokenArray['not_before'] )? (int)$tokenArray['not_before'] : null; 69 | $token->resource = $tokenArray['resource'] ?? null; 70 | $token->token = $tokenArray['access_token'] ?? null; 71 | 72 | return $token; 73 | } 74 | 75 | /** 76 | * Tells whether the token is not expired. 77 | * 78 | * @param int|null $time Specify time to check the token against. Default is current time. 79 | * 80 | * @return bool 81 | */ 82 | public function isValid( int $time = null ): bool { 83 | if ( $time === null ) { 84 | $time = time(); 85 | } 86 | 87 | return ( $time >= $this->notBefore && $time < $this->expiresOn ); 88 | } 89 | 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/Xrm/AttributeState.php: -------------------------------------------------------------------------------- 1 | attributes as $attribute => &$state ) { 40 | $state = false; 41 | } 42 | } 43 | 44 | /** 45 | * Whether a offset exists. 46 | * 47 | * @param mixed $offset An offset to check for. 48 | * 49 | * @return boolean true on success or false on failure. 50 | */ 51 | public function offsetExists( $offset ): bool { 52 | return array_key_exists( $offset, $this->attributes ); 53 | } 54 | 55 | /** 56 | * Offset to retrieve. 57 | * 58 | * @param mixed $offset The offset to retrieve. 59 | * 60 | * @return mixed Can return all value types. 61 | */ 62 | public function offsetGet( $offset ) { 63 | return array_key_exists( $offset, $this->attributes ) && $this->attributes[$offset] === true; 64 | } 65 | 66 | /** 67 | * Offset to set. 68 | * 69 | * @param mixed $offset The offset to assign the value to. 70 | * @param mixed $value The value to set. 71 | * 72 | * @return void 73 | */ 74 | public function offsetSet( $offset, $value ): void { 75 | $this->attributes[$offset] = $value; 76 | } 77 | 78 | /** 79 | * Offset to unset. 80 | * @param mixed $offset The offset to unset. 81 | * 82 | * @return void 83 | */ 84 | public function offsetUnset( $offset ): void { 85 | unset( $this->attributes[$offset] ); 86 | } 87 | 88 | /** 89 | * Retrieve an external iterator. 90 | * 91 | * @return Traversable An instance of an object implementing Iterator or Traversable. 92 | */ 93 | public function getIterator(): Traversable { 94 | return new \ArrayIterator( $this->attributes ); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/WebAPI/OData/Settings.php: -------------------------------------------------------------------------------- 1 | cachePool = new NullAdapter(); 89 | $this->logger = new NullLogger(); 90 | } 91 | 92 | /** 93 | * Sets a logger instance on the object. 94 | * 95 | * @param LoggerInterface $logger 96 | */ 97 | public function setLogger( LoggerInterface $logger ): void { 98 | $this->logger = $logger; 99 | } 100 | 101 | /** 102 | * Returns Web API endpoint URI. 103 | * 104 | * @return string 105 | */ 106 | public abstract function getEndpointURI(): string; 107 | 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamics Web API Toolkit 2 | 3 | The Dynamics Web API Toolkit provides an easy-to-use PHP wrapper for the [Dynamics 365 Customer Engagement Web API](https://docs.microsoft.com/en-au/dynamics365/customer-engagement/developer/use-microsoft-dynamics-365-web-api). 4 | 5 | Create, read, update and delete CRM records easily via the [IOrganizationService](https://msdn.microsoft.com/en-us/library/microsoft.xrm.sdk.iorganizationservice.aspx) - compatible interface, as well as execute Web API actions and functions. 6 | 7 | See [the tutorial](https://github.com/AlexaCRM/dynamics-webapi-toolkit/wiki/Tutorial) for the sample code to instantiate the connection, create, retrieve, update and delete records. 8 | 9 | This toolkit supports **only** Dynamics 365 Web API. For PHP implementation of the Dynamics 365 SOAP interface, see [php-crm-toolkit project](https://github.com/AlexaCRM/php-crm-toolkit). 10 | 11 | ## Features & Limitations 12 | 13 | The current release of the library does not support the following features (supported features and scenarios are mentioned along the way): 14 | 15 | - Authentication support for IFD and On-Premises (AD) deployments. Support for IFD (Internet Facing Deployment) is on the [roadmap](https://github.com/AlexaCRM/dynamics-webapi-toolkit/projects/1), On-Premises deployments (using AD) are under consideration. 16 | - Execute method of IOrganizationService interface is not supported yet. Means for executing functions and actions, both bound and unbound, are provided though. 17 | - Batch requests are not supported yet. That means, associating/disassociating multiple records is executed in multiple separate requests which may affect the performance. 18 | - Organization metadata (entities and attributes, global option sets, etc.) is not supported yet, although can be retrieved manually via the built-in OData helper client or via the HTTP client. 19 | - Most of the record attribute values are returned as-is from Web API. That means, at this point you must expect integers in place of OptionSetValue objects for picklist values, Status/State attributes, booleans for "Two Options" attributes, and floats for decimal and Money attributes. Lookup attribute values are rendered as EntityReference objects. The same applies when you set values in the Entity, including EntityReferences. This is likely to change once organization metadata is integrated into the toolkit. 20 | 21 | ## Getting Started 22 | 23 | ### Prerequisites 24 | 25 | The main requirement is PHP 7.4 or later. cURL is recommended but is not required. [Composer](https://getcomposer.org/) is required to install the toolkit and its dependencies. 26 | 27 | ### Installing 28 | 29 | ``` 30 | $ composer require alexacrm/dynamics-webapi-toolkit:dev-master 31 | ``` 32 | 33 | ### Consuming 34 | 35 | See the [Tutorial](https://github.com/AlexaCRM/dynamics-webapi-toolkit/wiki/Tutorial) to learn how to consume the library. 36 | 37 | ## Development 38 | 39 | The version compatible with PHP 8.2 and above is now available as `v4.x-dev`. Please note that this version is still under development, and its use is at your own risk. You can install it with the following command: 40 | 41 | ``` 42 | $ composer require alexacrm/dynamics-webapi-toolkit:v4.x-dev 43 | ``` 44 | 45 | ## Built With 46 | 47 | * David Yack's [Xrm.Tools.CRMWebAPI](https://github.com/davidyack/Xrm.Tools.CRMWebAPI) - some code was borrowed as OData helper code 48 | * [Guzzle](https://github.com/guzzle/guzzle) - an extensible PHP HTTP client 49 | 50 | ## Versioning 51 | 52 | Toolkit uses [SemVer](http://semver.org/) for versioning. 53 | 54 | ## License 55 | 56 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 57 | -------------------------------------------------------------------------------- /src/Enum/ChoiceEnum.php: -------------------------------------------------------------------------------- 1 | getConstants(); 48 | 49 | if (PHP_VERSION_ID >= 70100) { 50 | $values = array_filter($values, function (string $k) use ($r) { 51 | return $r->getReflectionConstant($k)->isPublic(); 52 | }, ARRAY_FILTER_USE_KEY); 53 | } 54 | 55 | if (is_a($enumType, FlaggedEnum::class, true)) { 56 | $values = array_filter($values, function ($v) { 57 | return \is_int($v) && 0 === ($v & $v - 1) && $v > 0; 58 | }); 59 | } else { 60 | $values = array_filter($values, function ($v) { 61 | return \is_int($v) || \is_string($v); 62 | }); 63 | } 64 | 65 | self::$guessedValues[$enumType] = array_values($values); 66 | } 67 | 68 | return self::$guessedValues[$enumType]; 69 | } 70 | 71 | /** 72 | * @see ChoiceEnumTrait::choices() 73 | */ 74 | protected static function choices(): array 75 | { 76 | $enumType = static::class; 77 | 78 | if (!isset(self::$guessedReadables[$enumType])) { 79 | $constants = (new \ReflectionClass($enumType))->getConstants(); 80 | foreach (self::autodiscoveredValues() as $value) { 81 | $constantName = array_search($value, $constants, true); 82 | self::$guessedReadables[$enumType][$value] = $constantName; 83 | } 84 | } 85 | 86 | return self::$guessedReadables[$enumType]; 87 | } 88 | 89 | /** 90 | * In JSON, serialize as string. 91 | * 92 | * @return string 93 | */ 94 | public function jsonSerialize() { 95 | return $this->getReadable(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Xrm/IOrganizationService.php: -------------------------------------------------------------------------------- 1 | message = $response; 53 | if ( $inner !== null ) { 54 | $guzzleRequest = $inner->getRequest(); 55 | $guzzleResponse = $inner->getResponse(); 56 | $statusCode = ( $guzzleResponse !== null )? $guzzleResponse->getStatusCode() : 0; 57 | 58 | $this->code = (int)$statusCode; 59 | 60 | $level = (int) floor( $statusCode / 100); 61 | if ($level === 4) { 62 | $label = 'Client error'; 63 | } elseif ($level === 5) { 64 | $label = 'Server error'; 65 | } else { 66 | $label = 'Unsuccessful request'; 67 | } 68 | 69 | $uri = $guzzleRequest->getUri(); 70 | 71 | $this->message = sprintf( 72 | '%s: `%s %s` resulted in a `%s %s` response with the following message: `%s`', 73 | $label, 74 | $guzzleRequest->getMethod(), 75 | $uri, 76 | $statusCode, 77 | ( $guzzleResponse !== null )? $guzzleResponse->getReasonPhrase() : '', 78 | $response->message 79 | ); 80 | } 81 | 82 | $this->response = $response; 83 | $this->innerException = $inner; 84 | } 85 | 86 | /** 87 | * Returns the error details response given by the OData service. 88 | * 89 | * @return object 90 | */ 91 | public function getResponse() { 92 | return $this->response; 93 | } 94 | 95 | /** 96 | * Returns the underlying Guzzle-generated exception with relevant request and response objects. 97 | * 98 | * @return RequestException 99 | */ 100 | public function getInnerException() { 101 | return $this->innerException; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/Xrm/Metadata/AttributeTypeCode.php: -------------------------------------------------------------------------------- 1 | key = $key; 42 | } 43 | 44 | /** 45 | * Returns the key for the current cache item. 46 | * 47 | * The key is loaded by the Implementing Library, but should be available to 48 | * the higher level callers when needed. 49 | * 50 | * @return string 51 | * The key string for this cache item. 52 | */ 53 | public function getKey() : string { 54 | return $this->key; 55 | } 56 | 57 | /** 58 | * Retrieves the value of the item from the cache associated with this object's key. 59 | * 60 | * The value returned must be identical to the value originally stored by set(). 61 | * 62 | * If isHit() returns false, this method MUST return null. Note that null 63 | * is a legitimate cached value, so the isHit() method SHOULD be used to 64 | * differentiate between "null value was found" and "no value was found." 65 | * 66 | * @return mixed 67 | * The value corresponding to this cache item's key, or null if not found. 68 | */ 69 | public function get() { 70 | return null; 71 | } 72 | 73 | /** 74 | * Confirms if the cache item lookup resulted in a cache hit. 75 | * 76 | * Note: This method MUST NOT have a race condition between calling isHit() 77 | * and calling get(). 78 | * 79 | * @return bool 80 | * True if the request resulted in a cache hit. False otherwise. 81 | */ 82 | public function isHit() : bool { 83 | return false; 84 | } 85 | 86 | /** 87 | * Sets the value represented by this cache item. 88 | * 89 | * The $value argument may be any item that can be serialized by PHP, 90 | * although the method of serialization is left up to the Implementing 91 | * Library. 92 | * 93 | * @param mixed $value 94 | * The serializable value to be stored. 95 | * 96 | * @return static 97 | * The invoked object. 98 | */ 99 | public function set($value) { 100 | return $this; 101 | } 102 | 103 | /** 104 | * Sets the expiration time for this cache item. 105 | * 106 | * @param \DateTimeInterface|null $expiration 107 | * The point in time after which the item MUST be considered expired. 108 | * If null is passed explicitly, a default value MAY be used. If none is set, 109 | * the value should be stored permanently or for as long as the 110 | * implementation allows. 111 | * 112 | * @return static 113 | * The called object. 114 | */ 115 | public function expiresAt($expiration) { 116 | return $this; 117 | } 118 | 119 | /** 120 | * Sets the expiration time for this cache item. 121 | * 122 | * @param int|\DateInterval|null $time 123 | * The period of time from the present after which the item MUST be considered 124 | * expired. An integer parameter is understood to be the time in seconds until 125 | * expiration. If null is passed explicitly, a default value MAY be used. 126 | * If none is set, the value should be stored permanently or for as long as the 127 | * implementation allows. 128 | * 129 | * @return static 130 | * The called object. 131 | */ 132 | public function expiresAfter($time) { 133 | return $this; 134 | } 135 | } 136 | --------------------------------------------------------------------------------