= 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]]