├── .gitignore ├── phpunit.xml.dist ├── .travis.yml ├── src ├── Model │ ├── CustomEvent.php │ ├── EntityEvent.php │ └── AbstractEvent.php ├── EngineClient.php └── EventClient.php ├── tests └── EngineClientTest.php ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | 8 | matrix: 9 | fast_finish: true 10 | 11 | env: 12 | - COMPOSER_FLAGS="--prefer-lowest" 13 | - COMPOSER_FLAGS="" 14 | 15 | before_install: 16 | - composer self-update 17 | 18 | install: 19 | - composer update --no-interaction --prefer-dist $COMPOSER_FLAGS 20 | 21 | script: 22 | - vendor/bin/phpunit 23 | 24 | notifications: 25 | email: 26 | - info@endroid.nl 27 | -------------------------------------------------------------------------------- /src/Model/CustomEvent.php: -------------------------------------------------------------------------------- 1 | setEventType(self::EVENT_TYPE_CUSTOM); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function supportsTargetEntity() 27 | { 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/EngineClientTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Endroid\Guide\Tests; 13 | 14 | use Endroid\PredictionIo\EngineClient; 15 | use GuzzleHttp\Exception\ConnectException; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class EngineClientTest extends TestCase 19 | { 20 | public function testConnectException() 21 | { 22 | $engineClient = new EngineClient('http://localhost:8000'); 23 | 24 | $this->expectException(ConnectException::class); 25 | $engineClient->getRecommendedItems('user_1', 3); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "endroid/prediction-io", 3 | "description": "Endroid PredictionIo Client", 4 | "keywords": ["endroid", "prediction", "io", "predictionio", "php"], 5 | "homepage": "https://github.com/endroid/prediction-io", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Jeroen van den Enden", 11 | "email": "info@endroid.nl" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.1", 16 | "endroid/installer": "^1.0.3", 17 | "predictionio/predictionio": "^0.9", 18 | "guzzlehttp/guzzle": ">=6.3.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^5.7|^6.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Endroid\\PredictionIo\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Endroid\\PredictionIo\\Tests\\": "tests/" 31 | } 32 | }, 33 | "extra": { 34 | "branch-alias": { 35 | "dev-master": "3.x-dev" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Jeroen van den Enden 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/Model/EntityEvent.php: -------------------------------------------------------------------------------- 1 | checkEvent($event); 22 | parent::__construct($event, $entityType, $entityId); 23 | $this->setEventType(self::EVENT_TYPE_ENTITY); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function supportsTargetEntity() 30 | { 31 | return false; 32 | } 33 | 34 | /** 35 | * @param $event 36 | * 37 | * @throws \InvalidArgumentException 38 | */ 39 | public function checkEvent($event) 40 | { 41 | if (!in_array($event, self::$AVAILABLE_EVENTS)) { 42 | throw new \InvalidArgumentException(sprintf('Unsupported event: `%s`, available events: `%s`', 43 | $event, 44 | implode(', ', self::$AVAILABLE_EVENTS) 45 | ) 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/EngineClient.php: -------------------------------------------------------------------------------- 1 | client = new Client([ 25 | 'base_uri' => $baseUrl, 26 | 'timeout' => $timeout, 27 | 'connect_timeout' => $connectTimeout, 28 | 'verify' => false, 29 | ]); 30 | } 31 | 32 | /** 33 | * Returns the recommendations for the given user. 34 | * 35 | * @param string $userId 36 | * @param int $itemCount 37 | * 38 | * @return string JSON response 39 | */ 40 | public function getRecommendedItems($userId, $itemCount = 3) 41 | { 42 | $response = $this->sendQuery(['user' => $userId, 'num' => intval($itemCount)]); 43 | 44 | return $response; 45 | } 46 | 47 | /** 48 | * Returns the items similar to the given item. 49 | * 50 | * @param string $items 51 | * @param int $itemCount 52 | * 53 | * @return string JSON response 54 | */ 55 | public function getSimilarItems($items, $itemCount = 3) 56 | { 57 | if (!is_array($items)) { 58 | $items = [$items]; 59 | } 60 | 61 | $response = $this->sendQuery(['items' => $items, 'num' => intval($itemCount)]); 62 | 63 | return $response; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/EventClient.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Endroid\PredictionIo; 13 | 14 | use DateTime; 15 | use predictionio\EventClient as BaseEventClient; 16 | use Endroid\PredictionIo\Model\CustomEvent; 17 | use Endroid\PredictionIo\Model\EntityEvent; 18 | 19 | class EventClient extends BaseEventClient 20 | { 21 | /** 22 | * @param string $event 23 | * @param string $entityType 24 | * @param string $entityId 25 | * @param array $properties 26 | * @param null|DateTime $eventTime 27 | * 28 | * @return string JSON response 29 | */ 30 | public function createEntityEvent($event, $entityType, $entityId, array $properties = [], DateTime $eventTime = null) 31 | { 32 | $event = new EntityEvent($event, $entityType, $entityId); 33 | $event->setProperties($properties); 34 | $event->setEventTime($eventTime); 35 | $response = $this->createEvent($event->toArray()); 36 | 37 | return $response; 38 | } 39 | 40 | /** 41 | * @param string $event 42 | * @param string $entityType 43 | * @param string $entityId 44 | * @param string $targetEntityType 45 | * @param string $targetEntityId 46 | * @param array $properties 47 | * @param null|DateTime $eventTime 48 | * 49 | * @return string JSON response 50 | */ 51 | public function createCustomEvent($event, $entityType, $entityId, $targetEntityType, $targetEntityId, array $properties = [], DateTime $eventTime = null) 52 | { 53 | $event = new CustomEvent($event, $entityType, $entityId); 54 | $event->setProperties($properties); 55 | $event->setTargetEntityType($targetEntityType); 56 | $event->setTargetEntityId($targetEntityId); 57 | $event->setEventTime($eventTime); 58 | $response = $this->createEvent($event->toArray()); 59 | 60 | return $response; 61 | } 62 | 63 | /** 64 | * Create a user. 65 | * 66 | * @param string $userId 67 | * @param array $properties 68 | * 69 | * @return string JSON response 70 | */ 71 | public function createUser($userId, array $properties = []) 72 | { 73 | return $this->createEntityEvent('$set', 'user', $userId, $properties); 74 | } 75 | 76 | /** 77 | * Create an item. 78 | * 79 | * @param string $itemId 80 | * @param array $properties 81 | * 82 | * @return string JSON response 83 | */ 84 | public function createItem($itemId, $properties = []) 85 | { 86 | return $this->createEntityEvent('$set', 'item', $itemId, $properties); 87 | } 88 | 89 | /** 90 | * Record a user action on an item. 91 | * 92 | * @param string $action 93 | * @param string $userId 94 | * @param string $itemId 95 | * @param array $properties 96 | * @param null $eventTime 97 | * 98 | * @return string JSON response 99 | */ 100 | public function recordUserActionOnItem($action = 'view', $userId, $itemId, array $properties = [], $eventTime = null) 101 | { 102 | return $this->createCustomEvent($action, 'user', $userId, 'item', $itemId, $properties, $eventTime); 103 | } 104 | 105 | /** 106 | * Set specific items to unavailable. 107 | * 108 | * @param array $items 109 | * 110 | * @return string JSON response 111 | */ 112 | public function setUnavailableItems(array $items) 113 | { 114 | return $this->createEntityEvent('$set', 'constraint', 'unavailableItems', ['items' => $items]); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prediction IO 2 | 3 | *By [endroid](https://endroid.nl/)* 4 | 5 | [![Build Status](http://img.shields.io/travis/endroid/prediction-io.svg)](http://travis-ci.org/endroid/prediction-io) 6 | [![Latest Stable Version](http://img.shields.io/packagist/v/endroid/prediction-io.svg)](https://packagist.org/packages/endroid/prediction-io) 7 | [![Total Downloads](http://img.shields.io/packagist/dt/endroid/prediction-io.svg)](https://packagist.org/packages/endroid/prediction-io) 8 | [![License](http://img.shields.io/packagist/l/endroid/prediction-io.svg)](https://packagist.org/packages/endroid/prediction-io) 9 | 10 | The Prediction IO library provides a client which offers easy access to a PredictionIo recommendation engine. 11 | PredictionIo is an open source machine learning server for software developers to create predictive features, such as 12 | personalization, recommendation and content discovery. 13 | 14 | Through a small set of simple calls, all server functionality is exposed to your application. You can add users and items, 15 | register actions between these users and items and retrieve recommendations deduced from this information by any 16 | [`PredictionIo`](http://prediction.io/) recommendation engine. Applications range from showing recommended products in a 17 | web shop to discovering relevant experts in a social collaboration network. 18 | 19 | ![Recommendations](https://raw.githubusercontent.com/endroid/PredictionIo/master/src/Bundle/Resources/public/images/recommendations.png) 20 | 21 | ## Requirements 22 | 23 | * Symfony 24 | * Dependencies: 25 | * [`PredictionIo-PHP-SDK`](https://github.com/PredictionIo/PredictionIo-PHP-SDK) 26 | 27 | ## Installation 28 | 29 | Use [Composer](https://getcomposer.org/) to install the library. 30 | 31 | ``` bash 32 | $ composer require endroid/prediction-io 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```php 38 | use Endroid\PredictionIo\EventClient; 39 | use Endroid\PredictionIo\EngineClient; 40 | 41 | $apiKey = '...'; 42 | $eventClient = new EventClient($apiKey); 43 | $recommendationEngineClient = new EngineClient('http://localhost:8000'); 44 | $similarProductEngineClient = new EngineClient('http://localhost:8001'); 45 | 46 | // Populate with users and items 47 | $userProperties = ['address' => '1234 Street, San Francisco, CA 94107', 'birthday' => '22-04-1991']; 48 | $eventClient->createUser('user_1', $userProperties); 49 | $itemProperties = ['categories' => [123, 1234, 12345]]; 50 | $eventClient->createItem('product_1', $itemProperties); 51 | 52 | // Record actions 53 | $actionProperties = ['firstView' => true]; 54 | $eventClient->recordUserActionOnItem('view', 'user_1', 'product_1', $actionProperties); 55 | 56 | // Return recommendations 57 | $itemCount = 20; 58 | $recommendedProducts = $recommendationEngineClient->getRecommendedItems('user_1', $itemCount); 59 | $similarProducts = $similarProductEngineClient->getSimilarItems('product_1', $itemCount); 60 | 61 | ``` 62 | 63 | ## Symfony integration 64 | 65 | Register the Symfony bundle in the kernel. 66 | 67 | ```php 68 | // app/AppKernel.php 69 | 70 | public function registerBundles() 71 | { 72 | $bundles = [ 73 | // ... 74 | new Endroid\PredictionIo\Bundle\PredictionIoBundle\EndroidPredictionIoBundle(), 75 | ]; 76 | } 77 | 78 | ``` 79 | 80 | The default parameters can be overridden via the configuration. 81 | 82 | ```yaml 83 | endroid_prediction_io: 84 | event_server: 85 | url: http://localhost:7070 86 | apps: 87 | app_one: 88 | key: '...' 89 | engines: 90 | recommendation: 91 | url: http://localhost:8000 92 | similarproduct: 93 | url: http://localhost:8001 94 | viewedthenbought: 95 | url: http://localhost:8002 96 | complementarypurchase: 97 | url: http://localhost:8003 98 | productranking: 99 | url: http://localhost:8004 100 | leadscoring: 101 | url: http://localhost:8005 102 | app_two: 103 | key: '...' 104 | engines: 105 | complementarypurchase: 106 | url: http://localhost:8006 107 | leadscoring: 108 | url: http://localhost:8007 109 | 110 | ``` 111 | 112 | Now you can retrieve the event and engine clients as follows. 113 | 114 | ```php 115 | /** @var EventClient $eventClient */ 116 | $eventClient = $this->get('endroid.prediction_io.app_one.event_client'); 117 | 118 | /** @var EngineClient $recommendationEngineClient */ 119 | $recommendationEngineClient = $this->get('endroid.prediction_io.app_one.recommendation.engine_client'); 120 | 121 | /** @var EngineClient $similarProductEngineClient */ 122 | $similarProductEngineCl![Recommendations](https://raw.githubusercontent.com/endroid/PredictionIo/master/assets/recommendations.png)ient = $this->get('endroid.prediction_io.app_one.similarproduct.engine_client'); 123 | 124 | ``` 125 | 126 | ## Docker 127 | 128 | Many Docker images exist for running a PredictionIo server. Personally I used the 129 | [`spereio`](https://github.com/sphereio/docker-predictionio) image to create an image 130 | that creates, trains and deploys a recommendation engine and starts the PIO server. It 131 | starts a cron that trains the model every 5 minutes. You can find that image 132 | [`here`](https://github.com/endroid/docker/tree/master/docker/prediction-io). 133 | 134 | ## Vagrant box 135 | 136 | PredictionIo provides a [`Vagrant box`](https://docs.prediction.io/install/install-vagrant/) 137 | containing an out-of-the-box PredictionIo server. 138 | 139 | ## Versioning 140 | 141 | Version numbers follow the MAJOR.MINOR.PATCH scheme. Backwards compatible 142 | changes will be kept to a minimum but be aware that these can occur. Lock 143 | your dependencies for production and test your code when upgrading. 144 | 145 | ## License 146 | 147 | This bundle is under the MIT license. For the full copyright and license 148 | information please view the LICENSE file that was distributed with this source code. -------------------------------------------------------------------------------- /src/Model/AbstractEvent.php: -------------------------------------------------------------------------------- 1 | event = $event; 58 | $this->entityType = $entityType; 59 | $this->entityId = $entityId; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getEvent() 66 | { 67 | return $this->event; 68 | } 69 | 70 | /** 71 | * @param $event 72 | * 73 | * @return $this 74 | */ 75 | public function setEvent($event) 76 | { 77 | $this->event = $event; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * @return string 84 | */ 85 | public function getEntityType() 86 | { 87 | return $this->entityType; 88 | } 89 | 90 | /** 91 | * @param $entityType 92 | * 93 | * @return $this 94 | */ 95 | public function setEntityType($entityType) 96 | { 97 | $this->entityType = $entityType; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * @return string 104 | */ 105 | public function getEntityId() 106 | { 107 | return $this->entityId; 108 | } 109 | 110 | /** 111 | * @param $entityId 112 | * 113 | * @return $this 114 | */ 115 | public function setEntityId($entityId) 116 | { 117 | $this->entityId = $entityId; 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * @return string 124 | */ 125 | public function getTargetEntityType() 126 | { 127 | return $this->targetEntityType; 128 | } 129 | 130 | /** 131 | * @param $targetEntityType 132 | * 133 | * @throws \InvalidArgumentException 134 | * 135 | * @return $this 136 | */ 137 | public function setTargetEntityType($targetEntityType) 138 | { 139 | if (!$this->supportsTargetEntity()) { 140 | throw new \InvalidArgumentException(sprintf('Event of type `%s` does not support setting a target entity', 141 | $this->getEventType() 142 | ) 143 | ); 144 | } 145 | $this->targetEntityType = $targetEntityType; 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * @return string 152 | */ 153 | public function getTargetEntityId() 154 | { 155 | return $this->targetEntityId; 156 | } 157 | 158 | /** 159 | * @param $targetEntityId 160 | * 161 | * @throws \InvalidArgumentException 162 | * 163 | * @return $this 164 | */ 165 | public function setTargetEntityId($targetEntityId) 166 | { 167 | if (!$this->supportsTargetEntity()) { 168 | throw new \InvalidArgumentException(sprintf('Event of type `%s` does not support setting a target entity', 169 | $this->getEventType() 170 | ) 171 | ); 172 | } 173 | $this->targetEntityId = $targetEntityId; 174 | 175 | return $this; 176 | } 177 | 178 | /** 179 | * @return array 180 | */ 181 | public function getProperties() 182 | { 183 | return $this->properties; 184 | } 185 | 186 | /** 187 | * @param array $properties 188 | * 189 | * @return $this 190 | */ 191 | public function setProperties(array $properties) 192 | { 193 | $this->properties = $properties; 194 | 195 | return $this; 196 | } 197 | 198 | /** 199 | * @param bool $formatted Returns event time in ISO-8601 format 200 | * 201 | * @return \DateTime 202 | */ 203 | public function getEventTime($formatted = true) 204 | { 205 | if ($formatted && $this->eventTime instanceof \DateTime) { 206 | return $this->eventTime->format('c'); 207 | } 208 | 209 | return $this->eventTime; 210 | } 211 | 212 | /** 213 | * @param $eventTime 214 | * 215 | * @return $this 216 | */ 217 | public function setEventTime($eventTime) 218 | { 219 | $this->eventTime = $eventTime; 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * @return string 226 | */ 227 | public function getEventType() 228 | { 229 | return $this->_eventType; 230 | } 231 | 232 | /** 233 | * @param $eventType 234 | * 235 | * @return $this 236 | */ 237 | public function setEventType($eventType) 238 | { 239 | $this->_eventType = $eventType; 240 | 241 | return $this; 242 | } 243 | 244 | /** 245 | * @return bool 246 | */ 247 | abstract public function supportsTargetEntity(); 248 | 249 | /** 250 | * @return bool 251 | */ 252 | public function isEntityEvent() 253 | { 254 | return self::EVENT_TYPE_ENTITY == $this->getEventType(); 255 | } 256 | 257 | /** 258 | * @return bool 259 | */ 260 | public function isCustomEvent() 261 | { 262 | return self::EVENT_TYPE_CUSTOM == $this->getEventType(); 263 | } 264 | 265 | /** 266 | * @return array 267 | */ 268 | public function toArray() 269 | { 270 | $arrarizedEvent = []; 271 | $arrarizedEvent['event'] = $this->getEvent(); 272 | $arrarizedEvent['entityType'] = $this->getEntityType(); 273 | $arrarizedEvent['entityId'] = $this->getEntityId(); 274 | if ($this->supportsTargetEntity()) { 275 | if ($targetEntityType = $this->getTargetEntityType()) { 276 | $arrarizedEvent['targetEntityType'] = $targetEntityType; 277 | } 278 | if ($targetEntityId = $this->getTargetEntityId()) { 279 | $arrarizedEvent['targetEntityId'] = $targetEntityId; 280 | } 281 | } 282 | $properties = $this->getProperties(); 283 | if (!empty($properties)) { 284 | $arrarizedEvent['properties'] = $properties; 285 | } 286 | if ($eventTime = $this->getEventTime()) { 287 | $arrarizedEvent['eventTime'] = $eventTime; 288 | } 289 | 290 | return $arrarizedEvent; 291 | } 292 | } 293 | --------------------------------------------------------------------------------