= Yii 2 RESTful API Setup = * Download and setup ''advanced'' Yii 2 application. * Download [[https://github.com/deerawan/yii2-advanced-api]], and extract and copy ''[app]/api'' folder to our application (at the same folder level). * Advanced App: The ideas is to create API folder at same level as ''frontend'' and ''backend'' folders. Eg: backend common frontend ... api -- common ------ controllers ------ models -- config -- modules ------ v1 ---------- controllers ---------- models ------ v2 ---------- controllers ---------- models -- runtime -- tests -- web * Basic App: The ideas is to create API folder at same level as ''controllers'' and ''models'' folders. Eg: controllers models views ... api -- config -- modules ------ v1 ---------- controllers ---------- models ------ v2 ---------- controllers ---------- models -- runtime -- tests -- web Reference: [[https://github.com/deerawan/yii2-advanced-api]] == Create Models == Create the table ''distributor''. Eg: CREATE TABLE `distributor` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `first_name` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `last_name` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `name_prefix` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8_bin', `occupation` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `company_name` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `address` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `city` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `state_prov` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `postal_code` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `country` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `latitude` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `longitude` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `phone` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `fax` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8_bin', `email` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `website` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `services` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `hours` TEXT NULL COLLATE 'utf8_bin', `instructions` TEXT NULL COLLATE 'utf8_bin', `status` SMALLINT(6) NOT NULL DEFAULT '1', `created_by` INT(11) NOT NULL, `created_at` DATETIME NOT NULL, `updated_by` INT(11) NOT NULL, `updated_at` DATETIME NOT NULL, PRIMARY KEY (`id`) ) COLLATE='utf8_unicode_ci' ENGINE=InnoDB AUTO_INCREMENT=27 ; For our table ''distributor'', we create the following model in ''[app]/api/modules/v1/models'': == Create Controllers == For our model ''distributor'', we create the controller in ''[app]/api/modules/v1/controllers'': == Setup Configuration == We need to add references to our API ''v1'' module in the configuration file in ''[app]/api/config/main.php'': 'app-api', 'basePath' => dirname(__DIR__), // equivalent to [app]\api 'bootstrap' => ['log'], // // Define modules API // 'modules' => [ // API v1 'v1' => [ 'basePath' => '@app/modules/v1', 'class' => 'api\modules\v1\Module' ], // API v2 'v2' => [ 'basePath' => '@app/modules/v2', 'class' => 'api\modules\v2\Module' ] ], 'components' => [ 'user' => [ //'identityClass' => 'common\models\User', 'identityClass' => 'api\modules\v1\models\User', 'enableAutoLogin' => false, ], 'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0, 'targets' => [ [ 'class' => 'yii\log\FileTarget', 'levels' => ['error', 'warning'], ], ], ], // NOTE: If strict parsing is enabled, the incoming requested URL must match // at least one of the rules in order to be treated as a valid request, // or a yii\web\NotFoundHttpException will be thrown. If strict parsing // is disabled, when none of the rules matches the requested URL, // the path info part of the URL will be treated as the requested route. 'urlManager' => [ 'enablePrettyUrl' => true, 'enableStrictParsing' => true, 'showScriptName' => false, 'rules' => [ ['class' => 'yii\rest\UrlRule', 'controller' => 'v1/item-product'], [ 'class' => 'yii\rest\UrlRule', 'controller' => 'v1/distributor', //'tokens' => [ // '{id}' => '' //], //'except' => ['delete', 'create', 'update'], ], //[ // //----------------------------------------------------------------------------- // // LEGEND: 'pattern' => 'route' // // See more: http://www.yiiframework.com/doc-2.0/guide-runtime-routing.html // // RegEx notation in use: // // \d Any digit. // // \d+ One or more of a digit. // // \w Any word/alphanumeric character (letter, number, underscore). // // - Literal dash in controller name. // // [\w-]+ One or more alphanumeric or dash characters. // //----------------------------------------------------------------------------- // 'dashboard' => 'site/index', // // // RESTful rules (Examples) // 'entry/' => 'entry/view', // same as 'GET entry/' => 'entry/view', // 'PUT entry/' => 'entry/update' // 'POST,PUT entry/index' => 'entry/create' // 'POST s' => '/create', // 's' => '/index', // 'PUT /' => '/update', // 'DELETE /' => '/delete', // '/' => '/view', // // '/create' => '/create', // '//' => '/', // '/' => '/view', // 's' => '/index', //], // //['class' => 'yii\rest\UrlRule', 'controller' => 'user'], is equivalent to: //[ // 'PUT,PATCH users/' => 'user/update', // 'DELETE users/' => 'user/delete', // 'GET,HEAD users/' => 'user/view', // 'POST users' => 'user/create', // 'GET,HEAD users' => 'user/index', // 'users/' => 'user/options', // 'users' => 'user/options', //], ], ], 'formatter' => [ 'class' => 'yii\i18n\Formatter', 'dateFormat' => 'php:M d, Y', //'datetimeFormat' => 'php:M d, Y H:i:s', 'timeFormat' => 'php:H:i:s', 'defaultTimeZone' => 'America/New_York' ], 'request' => [ 'parsers' => [ 'application/json' => 'yii\web\JsonParser', ] ] ], 'params' => $params, ]; Include local configuration settings for the database in ''[app]/api/config/main-local.php'': [ 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=acme_website', 'username' => 'acmeusr', 'password' => 'acmepass', 'charset' => 'utf8', ], 'mailer' => [ 'class' => 'yii\swiftmailer\Mailer', 'viewPath' => '@common/mail', ], ], ]; Add any global params in configuration file ''[app]/api/config/params.php'': 'admin@example.com', ]; Add any local params in configuration file ''[app]/api/config/params-local.php'': == Add API Alias == Create file and add an alias in ''[app]/common/config/aliases.php'' (Advanced app) or ''[app]/config/aliases.php'' (Basic app): Or simply add the following line to the file ''[app]/common/config/bootstrap.php'' (Advanced app) or ''[app]/config/web.php'' (Basic app): ... // Method 1: //Yii::setAlias('@api', dirname(__DIR__) . '/api'); // for Basic app //Yii::setAlias('@api', dirname(dirname(__DIR__)) . '/api'); // for Advanced app // Method 2: $config = [ //... 'aliases' => [ //... '@api'=> dirname(__DIR__) . '/api', ], ]; == Setup Web Folder == Create file ''[app]/api/web/.htaccess'' (very important!!): # use mod_rewrite for pretty URL support RewriteEngine on # If a directory or a file exists, use the request directly RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d # Otherwise forward the request to index.php RewriteRule . index.php Create ''[app]/api/web/index.php''. For Advanced app: run(); For Basic app: run(); Create file ''[app]/api/web/robots.txt'': User-Agent: * Disallow: / Create file ''[app]/api/web/css/site.css'': html, body { height: 100%; } .wrap { min-height: 100%; height: auto; margin: 0 auto -60px; padding: 0 0 60px; } .wrap > .container { padding: 70px 15px 20px; } .footer { height: 60px; background-color: #f5f5f5; border-top: 1px solid #ddd; padding-top: 20px; } .jumbotron { text-align: center; background-color: transparent; } .jumbotron .btn { font-size: 21px; padding: 14px 24px; } .not-set { color: #c55; font-style: italic; } /* add sorting icons to gridview sort links */ a.asc:after, a.desc:after { position: relative; top: 1px; display: inline-block; font-family: 'Glyphicons Halflings'; font-style: normal; font-weight: normal; line-height: 1; padding-left: 5px; } a.asc:after { content: /*"\e113"*/ "\e151"; } a.desc:after { content: /*"\e114"*/ "\e152"; } .sort-numerical a.asc:after { content: "\e153"; } .sort-numerical a.desc:after { content: "\e154"; } .sort-ordinal a.asc:after { content: "\e155"; } .sort-ordinal a.desc:after { content: "\e156"; } .grid-view th { white-space: nowrap; } .hint-block { display: block; margin-top: 5px; color: #999; } .error-summary { color: #a94442; background: #fdf7f7; border-left: 3px solid #eed3d7; padding: 10px 20px; margin: 0 0 15px 0; } == Using HTTP Basic Authentication == When using the Basic application template: Eg: ... api -- config -- modules ------ v1 ---------- controllers ---------- models ------ v2 ---------- controllers ---------- models -- runtime ------ cache ------ debug ------ logs -- web ------ assets ------ css assets commands config controllers mail migrations models runtime tests vendor views web ''[app]/api/config/main-local.php'' [ 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=acme_productionlog', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ], 'mailer' => [ 'class' => 'yii\swiftmailer\Mailer', //'viewPath' => '@common/mail', 'viewPath' => '@app/mail', ], ], ]; ''[app]/api/config/main.php'' 'app-api', 'basePath' => dirname(__DIR__), // equivalent to [app]\api 'bootstrap' => ['log'], 'modules' => [ // API v1 'v1' => [ 'basePath' => '@app/modules/v1', 'class' => 'api\modules\v1\Module' ], // API v2 'v2' => [ 'basePath' => '@app/modules/v2', 'class' => 'api\modules\v1\Module' ] ], 'components' => [ 'user' => [ //'identityClass' => 'app\models\User', 'identityClass' => 'api\modules\v1\models\User', 'enableAutoLogin' => false, ], 'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0, 'targets' => [ [ 'class' => 'yii\log\FileTarget', 'levels' => ['error', 'warning'], ], ], ], 'urlManager' => [ 'enablePrettyUrl' => true, 'enableStrictParsing' => true, 'showScriptName' => false, 'rules' => [ ['class' => 'yii\rest\UrlRule', 'controller' => 'v1/item-product'], [ 'class' => 'yii\rest\UrlRule', 'controller' => 'v1/entry', //'tokens' => [ // '{id}' => '' //] ], ], ], 'formatter' => [ 'class' => 'yii\i18n\Formatter', 'dateFormat' => 'php:M d, Y', //'datetimeFormat' => 'php:M d, Y H:i:s', 'timeFormat' => 'php:H:i:s', 'defaultTimeZone' => 'America/New_York' ], 'request' => [ // Required for JSON input for API 'parsers' => [ 'application/json' => 'yii\web\JsonParser', ] ], 'response' => [ // Required for JSON output for API 'formatters' => [ \yii\web\Response::FORMAT_JSON => [ 'class' => 'yii\web\JsonResponseFormatter', 'prettyPrint' => true, //YII_DEBUG, // use "pretty" output in debug mode 'encodeOptions' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ], ], ], ], 'params' => $params, ]; ''[app]/api/config/params-local.php'' ''[app]/api/config/params.php'' 'admin@example.com', ]; ''[app]/api/modules/v1/Module.php'' user->enableSession = false; // required for API authentication //\Yii::$app->user->loginUrl = ['site/login']; // default to use without API authentication \Yii::$app->user->loginUrl = null; // required for API authentication // Authentication example: // http://100-token@localhost:8080/production-log/api/web/v1/entries/1 // http://[access_token]@localhost:8080/production-log/api/web/v1/entries/1 } } ''[app]/api/modules/v1/controllers/EntryController.php'' HttpBasicAuth::className(), // Composite Authentication //'class' => CompositeAuth::className(), //'authMethods' => [ // HttpBasicAuth::className(), // HttpBearerAuth::className(), // QueryParamAuth::className(), //], ]; return $behaviors; } } ''[app]/api/modules/v1/models/Entry.php'' TimestampBehavior::className(), 'createdAtAttribute' => 'created_at', // OR 'create_time', to override default field name 'updatedAtAttribute' => 'updated_at', // OR 'update_time', to override default field name 'value' => new \yii\db\Expression('NOW()'), //'value' => new \yii\db\Expression($formattedCurDateTime), ], //[ // 'class' => BlameableBehavior::className(), // 'createdByAttribute' => 'created_by', // OR 'author_id', to override default field name // 'updatedByAttribute' => 'updated_by', // OR 'updater_id', to override default field name //], ]; } } ''[app]/api/modules/v1/models/User.php'' [ 'id' => '100', 'username' => 'admin', 'password' => 'admin', 'authKey' => 'test100key', 'accessToken' => '100-token', ], '101' => [ 'id' => '101', 'username' => 'demo', 'password' => 'demo', 'authKey' => 'test101key', //'accessToken' => '101-token', // For more security, use a token with a GUID. // Generate GUID: C:\> php -r "echo com_create_guid();" 'accessToken' => 'B34D5EFE-924B-4BCC-883C-AC22B47E4CD4-token', ], ]; /** * @inheritdoc */ public static function findIdentity($id) { return isset(self::$users[$id]) ? new static(self::$users[$id]) : null; } /** * @inheritdoc */ public static function findIdentityByAccessToken($token, $type = null) { foreach (self::$users as $user) { if ($user['accessToken'] === $token) { return new static($user); } } return null; } /** * Finds user by username * * @param string $username * @return static|null */ public static function findByUsername($username) { foreach (self::$users as $user) { if (strcasecmp($user['username'], $username) === 0) { return new static($user); } } return null; } /** * @inheritdoc */ public function getId() { return $this->id; } /** * @inheritdoc */ public function getAuthKey() { return $this->authKey; } /** * @inheritdoc */ public function validateAuthKey($authKey) { return $this->authKey === $authKey; } /** * Validates password * * @param string $password password to validate * @return boolean if password provided is valid for current user */ public function validatePassword($password) { return $this->password === $password; } } Add additional files in ''[app]/api/web'': * .htaccess * index.php * robots.txt * css/site.css ''[app]/api/web/index.php'' run(); == Test API == Call the API using the following URLs (assuming ''acme'' is the application name): * View distributor index: [[http://localhost:8080/acme/api/web/v1/distributors]] * View distributor 3: [[http://localhost:8080/acme/api/web/v1/distributors/3]] * View distributor index (using HTTP Basic authentication): [[https://my_access_token@localhost:8080/acme/api/web/v1/distributors]] Use [[https://curl.haxx.se/download.html|CURL]] to test API calls. Eg: In Windows: === Create record === C:\> curl -i -H "Accept:application/json" -H "Content-Type:application/json" -X POST "http://123-token@localhost/acme/api/web/v1/entries" -d "{\"serial_number\": \"0\", \"product_code\": \"prod_AcmeJewels\", \"product_data\": \"None\", \"workstation\": \"John-laptop\"}" NOTE: In Windows, for JSON data, always use double quotes (no single quotes), and escape them with a backslash if any quotes are embedded in a string. === List all records === C:\> curl -i -H "Accept:application/json" "http://123-token@localhost/acme/api/web/v1/entries" == Modifying JSON Response == There are several ways to impact the formatting of the JSON (or whatever method used) reponse. Using configuration in ''[app]/config/web.php'' (Basic app), ''[app]/common/config/main.php'' (Advanced app), or ''[app]/api/config/main.php'': ... $config = [ //... 'components' => [ //... 'response' => [ // Required for JSON output for API 'formatters' => [ \yii\web\Response::FORMAT_JSON => [ 'class' => 'yii\web\JsonResponseFormatter', 'prettyPrint' => true, //YII_DEBUG, // use "pretty" output in debug mode 'encodeOptions' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ], ], ], //... ], 'params' => $params, ]; ... Using modified JSON response after a search. Edit your model to include: data = $this->sanitizeStringForJson($this->data); parent::afterFind(); } //------------------------------------------------------------------------- // description: Strip extra characters, and quote string for JSON use. // Usage: Tool::sanitizeStringForJson($str); // parameters : $str // return : $str (formatted string) //------------------------------------------------------------------------- private function sanitizeStringForJson($str) { $str = str_replace("\\", "\\\\", $str); $str = str_replace('/', "\\/", $str); $str = str_replace('"', "\\".'"', $str); $str = str_replace("\b", "\\b", $str); $str = str_replace("\t", "\\t", $str); $str = str_replace("\n", "\\n", $str); $str = str_replace("\f", "\\f", $str); $str = str_replace("\r", "\\r", $str); $str = str_replace("\u", "\\u", $str); return '"'.$str.'"'; } } ?> See also: * [[http://www.yiiframework.com/doc-2.0/guide-rest-response-formatting.html|REST Response Formatting]] * [[https://github.com/samdark/yii2-cookbook/blob/master/book/response-formats.md|Creating Your Own Response Formats]] == Override an Action == In an API controller, you can override an action by disabling the default one, and creating a comparable action. Eg: //... use app\models\Entry; class ApiEntryController extends ActiveController { //... public function actions() { //$actions = parent::actions(); //// disable actions if needed //unset($actions['delete'], $actions['create']); // disable the "delete" and "create" actions //return $actions; return array_merge(parent::actions(), [ 'create' => null, // Disable create, to override it later with actionCreate() 'delete' => null, // Disable delete, to block access to it //... ]); } // Override default Create action with this implementation public function actionCreate() { $model = new Entry(); //if ($model->load(Yii::$app->request->post())) { if ($model->load(Yii::$app->getRequest()->getBodyParams(), '')) { $model->product_data_hash = md5($model->product_data); // massage some data before saving, if needed if ($model->save()) { return $model; } else { throw new ServerErrorHttpException('Failed to save the Entry object for unknown reason.'); } } return $model; } } == Add a Custom Action == In the API controller, add the action and ''findModel()'' functions. Eg: //... use app\models\Entry; use app\models\EntrySearch; use app\models\Raw; class ApiEntryController extends ActiveController { //... // Flag the specified record as "Processed" public function actionFlagProcessed($id) { $model = $this->findModel($id); $model->processed = true; if ($model->save()) { return $model; } else { throw new ServerErrorHttpException('Failed to save the Entry object for unknown reason.'); } return $model; } /** * Finds the Raw model based on its primary key value. * If the model is not found, a 404 HTTP exception will be thrown. * @param integer $id * @return Raw the loaded model * @throws NotFoundHttpException if the model cannot be found */ protected function findModel($id) { if (($model = Raw::findOne($id)) !== null) { return $model; } else { throw new NotFoundHttpException('The requested page does not exist.'); } } } Add ''rules'' to the ''urlManager'' component in the ''[app]/config/web.php'' (Basic app), or ''[app]/common/config/main.php'' (Advanced app). Eg: //... $config = [ 'id' => 'basic', 'basePath' => dirname(__DIR__), 'bootstrap' => ['log'], 'components' => [ //... 'urlManager' => [ 'enablePrettyUrl' => true, 'showScriptName' => false, 'enableStrictParsing' => false, 'rules' => [ [ 'class' => 'yii\rest\UrlRule', 'controller' => 'api-entry', // or 'v1/api-entry' when in /api/modules/v1/controllers/api-entry 'extraPatterns' => [ 'GET flag-printed/' => 'flag-printed', // action flag-printed 'GET flag-processed/' => 'flag-processed', ], ], ] ], ], 'params' => $params, ]; //... == References == * [[http://budiirawan.com/setup-restful-api-yii2/|Setup RESTful API in Yii2]] * [[https://github.com/deerawan/yii2-advanced-api/|GitHub: Yii2 advanced template with RESTful API setup]] * [[http://www.yiiframework.com/doc-2.0/guide-rest-quick-start.html|Yii2: Guide to RESTful Web Services]] * [[http://www.yiiframework.com/doc-2.0/guide-structure-modules.html|Yii2: Guide to Modules (Application Structure)]] * [[http://rest.elkstein.org/|Learn REST: A Tutorial]] * [[https://developer.xamarin.com/recipes/android/web_services/consuming_services/call_a_rest_web_service/|Xamarin: Call a REST Web Service (from Android Mobile App)]] * [[http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api|Best Practices for a Pragmatic RESTful API]] * [[https://www.diggin-data.de/dd-cms/blog/post/view/id/1004/name/Creating+a+REST+API+for+Yii2-basic-template|Creating a REST API for Yii2 Basic Template]] * [[https://stackoverflow.com/questions/32626212/how-to-create-a-rest-api-for-yii2-basic-template|StackOverflow: REST API for Yii2 Basic Template]]