Jump to content

API:Calling internally: Difference between revisions

From mediawiki.org
Content deleted Content added
No edit summary
Tags: Visual edit Mobile edit Mobile web edit
Marked this version for translation
 
(24 intermediate revisions by 13 users not shown)
Line 1: Line 1:
<languages/>
{{API}}
<translate>
<!--T:1-->
The API can be called internally from within PHP without going through an HTTP request.
The API can be called internally from within PHP without going through an HTTP request.


<!--T:2-->
However, application logic should avoid this, and directly access the PHP classes that are responsible for the respective functionality, instead of going through the API framework.
However, application logic should avoid this, and directly access the PHP classes that are responsible for the respective functionality, instead of going through the API framework.


<!--T:3-->
Internal API calls should generally only be used for testing purposes, and quick prototyping.
Internal API calls should generally only be used for testing purposes, and quick prototyping.


== From test suite code ==
== From test suite code == <!--T:4-->
</translate>

To call the API from within tests, the easiest way is to extend your test class from <code>ApiTestCase</code> (rather than <code>MediaWikiTestCase</code>). Then you can use <code>[https://doc.wikimedia.org/mediawiki-core/master/php/classApiTestCase.html#a64c7db9b05653558ad589f2649ae3a52 ApiTestCase::doApiRequest()]</code> as follows.
<translate><!--T:5--> To call the API from within tests, the easiest way is to extend your test class from <tvar name=1><code>ApiTestCase</code></tvar> (rather than <tvar name=2><code>MediaWikiTestCase</code></tvar>).</translate>
<translate><!--T:39--> Then you can use <tvar name=3><code>[https://doc.wikimedia.org/mediawiki-core/master/php/classApiTestCase.html#a64c7db9b05653558ad589f2649ae3a52 ApiTestCase::doApiRequest()]</code></tvar> as follows.</translate>


<syntaxhighlight lang=php>
<syntaxhighlight lang=php>
Line 18: Line 25:
</syntaxhighlight>
</syntaxhighlight>


<translate>
<!--T:6-->
This is how tests of the API should be done, but sometimes you want a test to ''use'' the API in order to set up for other tests that are not actual API tests. In this case you'll probably not want to inherit from ApiTestCase.
This is how tests of the API should be done, but sometimes you want a test to ''use'' the API in order to set up for other tests that are not actual API tests. In this case you'll probably not want to inherit from ApiTestCase.


<!--T:7-->
In this situation the request has to be constructed in more detail because the edit token needs to be stored in the session. In this case, the <code>$wgRequest</code> global is a FauxRequest that is already configured by the test harness.
In this situation the request has to be constructed in more detail because the edit token needs to be stored in the session. In this case, the <tvar name=1><code>$wgRequest</code></tvar> global is a FauxRequest that is already configured by the test harness.
</translate>


<syntaxhighlight lang=php>
<syntaxhighlight lang=php>
global $wgRequest;
global $wgRequest;
$user = parent::getTestSysop()->getUser();
$user = parent::getTestSysop()->getUser();
$this->assertTrue($user->isLoggedIn());
$this->assertTrue($user->isRegistered());
$apiParams = [
$apiParams = [
'action' => 'edit',
'action' => 'edit',
Line 40: Line 51:
</syntaxhighlight>
</syntaxhighlight>


<translate>
== From application code ==
== From application code == <!--T:8-->
</translate>
{{Note|1=<translate><!--T:9--> Calling the API internally is often a sign that some functionality should be refactored into a backend class that can be used both by your code and by the API module. It is discouraged in new production code and is considered technical debt. See <tvar name=1>[[Architecture guidelines#Separation of concerns — UI and business logic]]</tvar>.</translate>


<translate><!--T:10--> There are some acceptable cases when this technique is acceptable, however: unit and functional tests.</translate>
{{Note|1=Calling the API internally is often a sign that some functionality should be refactored into a backend class that can be used both by your code and by the API module. It is discouraged in new production code and is considered technical debt. See [[Architecture guidelines#Separation of concerns — UI and business logic]].

There are some acceptable cases when this technique is acceptable, however: unit and functional tests.
|2=gotcha}}
|2=gotcha}}
{{Note|1=If your code is making predetermined edits and does not need to to sanitize user input, go through the abuse filter, etc., consider using <code>[[Special:MyLanguage/Manual:WikiPage.php|WikiPage]]::doEditContent()</code> instead of an API request.}}
{{Note|1=<translate><!--T:11--> If your code is making predetermined edits and does not need to sanitize user input, go through the abuse filter, etc., consider using <tvar name=1><code>{{ll|Manual:WikiPage.php|WikiPage}}::doUserEditContent()</code></tvar> instead of an API request.</translate>}}


<translate>
<!--T:12-->
Sometimes other PHP code may wish to use the data access and aggregation functionality of the action API. Rather than making an HTTP network request to the same server, you can make a call within PHP.
Sometimes other PHP code may wish to use the data access and aggregation functionality of the action API. Rather than making an HTTP network request to the same server, you can make a call within PHP.


<!--T:13-->
The steps are:
The steps are:
</translate>


1) If you are executing in the context of an existing request from a user, prepare request parameters using the <code>DerivativeRequest</code> class.
1) <translate><!--T:14--> If you are executing in the context of an existing request from a user, prepare request parameters using the <tvar name=1><code>DerivativeRequest</code></tvar> class.</translate>
* Its constructor's first parameter is the request to derive from.
* <translate><!--T:15--> Its constructor's first parameter is the request to derive from.</translate>
* Its constructor's second parameter is an array of API parameters that is the same as if making the request over the web.
* <translate><!--T:16--> Its constructor's second parameter is an array of API parameters that is the same as if making the request over the web.</translate>
* Its constructor's third parameter is optional, specify <code>true</code> to treat the API call as a POST when the API module you're invoking requires POST requests.
* <translate><!--T:17--> Its constructor's third parameter is optional, specify <tvar name=1><code>true</code></tvar> to treat the API call as a POST when the API module you're invoking requires POST requests.</translate>


This sample code issues the 'allpages' list query starting at the letter 'M'.
<translate><!--T:18--> This sample code issues the 'allpages' list query starting at the letter 'M'.</translate>
This is a simple query, not requiring a user or POST.
<translate><!--T:19--> This is a simple query, not requiring a user or POST.</translate>

<source lang="php">
<syntaxhighlight lang="php">
$params = new DerivativeRequest(
$params = new DerivativeRequest(
$this->getRequest(), // Fallback upon $wgRequest if you can't access context.
$this->getRequest(), // <translate nowrap><!--T:36--> Fallback upon <tvar name=1>$wgRequest</tvar> if you can't access context.</translate>
array(
array(
'action' => 'query',
'action' => 'query',
'list' => 'allpages',
'list' => 'allpages',
'apnamespace' => 0,
'apnamespace' => 0,
'aplimit' => 10,
'aplimit' => 10,
'apprefix' => 'M'
'apprefix' => 'M'
)
)
);
);
</syntaxhighlight>
</source>


<translate>
<!--T:20-->
If you need to provide an edit token as an API parameter when making edits or other changes, you can get the edit token like so:
If you need to provide an edit token as an API parameter when making edits or other changes, you can get the edit token like so:
</translate>
<source lang="php">

$user = $this->getUser(); // Or User::newFromName, etc.
<syntaxhighlight lang="php">
$user = $this->getUser(); // <translate nowrap><!--T:35--> Or <tvar name=1>User::newFromName</tvar>, etc.</translate>
$token = $user->getEditToken();
$token = $user->getEditToken();
</syntaxhighlight>
</source>

2) <translate><!--T:21--> Create an <tvar name=1><code>ApiMain</code></tvar> instance.</translate>
<translate><!--T:40--> Then execute the API request.</translate>
<translate><!--T:41--> Because the parameter is a <tvar name=2><code>DerivativeRequest</code></tvar> object, <tvar name=3><code>ApiMain</code></tvar> will not execute any formatting printers, nor will it handle any errors.</translate>
<translate><!--T:42--> A parameter error or any other internal error will cause an exception that may be caught in the calling code.</translate>


<syntaxhighlight lang="php">
2) Create an <code>ApiMain</code> instance. Then execute the API request. Because the parameter is a <code>DerivativeRequest</code> object, <code>ApiMain</code> will not execute any formatting printers, nor will it handle any errors. A parameter error or any other internal error will cause an exception that may be caught in the calling code.
<source lang="php">
$api = new ApiMain( $params );
$api = new ApiMain( $params );
$api->execute();
$api->execute();
</syntaxhighlight>
</source>


<translate>
'''Important:''' If you want to ''create'' or ''edit'' pages, you have to pass <code>true</code> as a second parameter when creating the <code>ApiMain object</code>:
<!--T:22-->
<source lang="php">
'''Important:''' If you want to ''create'' or ''edit'' pages, you have to pass <tvar name=1><code>true</code></tvar> as a second parameter when creating the <tvar name=2><code>ApiMain object</code></tvar>:
$api = new ApiMain( $params, true ); // default is false
</translate>

<syntaxhighlight lang="php">
$api = new ApiMain( $params, true ); // <translate nowrap><!--T:37--> default is <tvar name=1>false</tvar></translate>
$api->execute();
$api->execute();
</syntaxhighlight>
</source>


3) Get the resulting data array.
3) <translate><!--T:23--> Get the resulting data array.</translate>
<source lang="php">
<syntaxhighlight lang="php">
$data = $api->getResult()->getResultData();
$data = $api->getResult()->getResultData();
</syntaxhighlight>
</source>


Here is a complete example taken from {{ll|Extension:WikiLove}} (as of {{rev|112758}}).
<translate><!--T:24--> Here is a complete example taken from <tvar name=1>{{ll|Extension:WikiLove}}</tvar> (as of <tvar name=2>[[gerrit:1050736]]</tvar>).</translate>
It adds text to a page, so it must run when handling a logged-in user's HTTP request.
<translate><!--T:25--> It adds text to a page, so it must run when handling a logged-in user's HTTP request.</translate>

<source lang="php">
<syntaxhighlight lang="php">
$api = new ApiMain(
// <translate nowrap><!--T:38--> Requires MediaWiki 1.19+</translate>
new DerivativeRequest(
$apiParamArray = [
$this->getRequest(), // Fallback upon $wgRequest if you can't access context
'action' => 'edit',
array(
'title' => $talk->getFullText(),
'action' => 'edit',
'section' => 'new',
'title' => $talk->getFullText(),
'sectiontitle' => $params['subject'],
'appendtext' => ( $talk->exists()
'text' => $params['text'],
? "\n\n"
'token' => $params['token'],
: '' ) .
'summary' => $summary,
wfMsgForContent( 'newsectionheaderdefaultlevel',
$params['subject'] )
'tags' => implode( '|', $params['tags'] ?? [] ),
'notminor' => true
. "\n\n" . $params['text'],
];
'token' => $params['token'],
}
'summary' => wfMsgForContent( 'wikilove-summary',

$wgParser->stripSectionName( $params['subject'] ) ),
$api = new ApiMain(
'notminor' => true
new DerivativeRequest(
),
$this->getRequest(),
true // treat this as a POST
$apiParamArray,
),
/* $wasPosted */ true
true // Enable write.
),
);
/* $enableWrite */ true
);

$api->execute();
</syntaxhighlight>


<translate>
$api->execute();
=== FauxRequest === <!--T:26-->
</source>
</translate>
<translate><!--T:27--> The example above creates a <tvar name=1><code>DerivativeRequest</code> {{ll|Manual:RequestContext.php|RequestContext}}</tvar>.</translate>
<translate><!--T:28--> This "inherits" some of the original request, such as IP and request headers that are set when MediaWiki is doing an action on behalf of a user, typically when handling a web request.</translate>
<translate><!--T:29--> If there is no user request context, for example when invoking the action API from a system process, or if you want to make a completely separate internal request, then you can use <tvar name=1><code>{{class doclink|FauxRequest}}</code></tvar> instead.</translate>


<translate>
=== FauxRequest ===
<!--T:30-->
The example above creates a <code>DerivativeRequest</code> {{ll|Manual:RequestContext.php|RequestContext}}.
Using <tvar name=1><code>FauxRequest</code></tvar> for write operations without passing request context causes [[<tvar name=2>phab:T36838</tvar>|bug T36838]].
This "inherits" some of the original request, such as IP and request headers that are set when MediaWiki is doing an action on behalf of a user, typically when handling a web request.
If there is no user request context, for example when invoking the action API from a system process, or if you want to make a completely separate internal request, then you can use <code>{{class doclink|FauxRequest}}</code> instead.


=== Error handling === <!--T:31-->
Using <code>FauxRequest</code> for write operations without passing request context causes [[phab:T36838|bug T36838]].
</translate>
{{Note|1=<translate><!--T:32--> If passed invalid parameters, the action API may throw a <tvar name=1><code>UsageException</code></tvar>. If its possible for your code to send an invalid parameter, you should probably call the API from inside a try/catch block</translate>}}


<translate>
=== Error handling ===
{{Note|1=If passed invalid parameters, the action API may throw a <code>UsageException</code>. If its possible for your code to send an invalid parameter, you should probably call the API from inside a try/catch block}}


== See also ==
== See also == <!--T:33-->
</translate>
* [[wikitech:Debugging in production#Debugging api.php in shell]] for how to debug api.php in WMF production
* <translate><!--T:34--> <tvar name=1>[[wikitech:Debugging in production#Debugging api.php in shell]]</tvar> for how to debug api.php in WMF production</translate>


[[Category:Testing{{#translation:}}]]
[[Category:Testing{{#translation:}}]]

Latest revision as of 05:40, 7 March 2026

The API can be called internally from within PHP without going through an HTTP request.

However, application logic should avoid this, and directly access the PHP classes that are responsible for the respective functionality, instead of going through the API framework.

Internal API calls should generally only be used for testing purposes, and quick prototyping.

From test suite code

[edit | edit source]

To call the API from within tests, the easiest way is to extend your test class from ApiTestCase (rather than MediaWikiTestCase). Then you can use ApiTestCase::doApiRequest() as follows.

$result = $this->doApiRequest( [
	'action'     => 'edit',
	'title'      => 'Test_page',
	'appendtext' => 'Testing editing.',
	'token'      => $user->getEditToken(),
] );

This is how tests of the API should be done, but sometimes you want a test to use the API in order to set up for other tests that are not actual API tests. In this case you'll probably not want to inherit from ApiTestCase.

In this situation the request has to be constructed in more detail because the edit token needs to be stored in the session. In this case, the $wgRequest global is a FauxRequest that is already configured by the test harness.

global $wgRequest;
$user = parent::getTestSysop()->getUser();
$this->assertTrue($user->isRegistered());
$apiParams = [
	'action'     => 'edit',
	'title'      => 'Test_page',
	'appendtext' => 'Testing editing.',
	'token'      => $user->getEditToken(),
];
$apiRequest = new FauxRequest( $apiParams, true, $wgRequest->getSessionArray() );
$context = new DerivativeContext( new RequestContext() );
$context->setRequest( $apiRequest );
$context->setUser( $user );
$api = new ApiMain( $context, true );
$result = $api->execute();

From application code

[edit | edit source]
Calling the API internally is often a sign that some functionality should be refactored into a backend class that can be used both by your code and by the API module. It is discouraged in new production code and is considered technical debt. See Architecture guidelines#Separation of concerns — UI and business logic. There are some acceptable cases when this technique is acceptable, however: unit and functional tests.
If your code is making predetermined edits and does not need to sanitize user input, go through the abuse filter, etc., consider using WikiPage ::doUserEditContent() instead of an API request.

Sometimes other PHP code may wish to use the data access and aggregation functionality of the action API. Rather than making an HTTP network request to the same server, you can make a call within PHP.

The steps are:

1) If you are executing in the context of an existing request from a user, prepare request parameters using the DerivativeRequest class.

  • Its constructor's first parameter is the request to derive from.
  • Its constructor's second parameter is an array of API parameters that is the same as if making the request over the web.
  • Its constructor's third parameter is optional, specify true to treat the API call as a POST when the API module you're invoking requires POST requests.

This sample code issues the 'allpages' list query starting at the letter 'M'. This is a simple query, not requiring a user or POST.

$params = new DerivativeRequest(
	$this->getRequest(), // Fallback upon $wgRequest if you can't access context.
	array(
		'action' => 'query',
		'list' => 'allpages',
		'apnamespace' => 0,
		'aplimit' => 10, 
		'apprefix' => 'M'
	)
);

If you need to provide an edit token as an API parameter when making edits or other changes, you can get the edit token like so:

$user = $this->getUser(); // Or User::newFromName, etc.
$token = $user->getEditToken();

2) Create an ApiMain instance. Then execute the API request. Because the parameter is a DerivativeRequest object, ApiMain will not execute any formatting printers, nor will it handle any errors. A parameter error or any other internal error will cause an exception that may be caught in the calling code.

$api = new ApiMain( $params );
$api->execute();

Important: If you want to create or edit pages, you have to pass true as a second parameter when creating the ApiMain object:

$api = new ApiMain( $params, true ); // default is false
$api->execute();

3) Get the resulting data array.

$data = $api->getResult()->getResultData();

Here is a complete example taken from Extension:WikiLove (as of gerrit:1050736). It adds text to a page, so it must run when handling a logged-in user's HTTP request.

// Requires MediaWiki 1.19+
        $apiParamArray = [
				'action' => 'edit',
				'title' => $talk->getFullText(),
				'section' => 'new',
				'sectiontitle' => $params['subject'],
				'text' => $params['text'],
				'token' => $params['token'],
				'summary' => $summary,
				'tags' => implode( '|', $params['tags'] ?? [] ),
				'notminor' => true
			];
		}

		$api = new ApiMain(
			new DerivativeRequest(
				$this->getRequest(),
				$apiParamArray,
				/* $wasPosted */ true
			),
			/* $enableWrite */ true
		);

		$api->execute();

FauxRequest

[edit | edit source]

The example above creates a DerivativeRequest RequestContext . This "inherits" some of the original request, such as IP and request headers that are set when MediaWiki is doing an action on behalf of a user, typically when handling a web request. If there is no user request context, for example when invoking the action API from a system process, or if you want to make a completely separate internal request, then you can use FauxRequest instead.

Using FauxRequest for write operations without passing request context causes bug T36838.

Error handling

[edit | edit source]
If passed invalid parameters, the action API may throw a UsageException. If its possible for your code to send an invalid parameter, you should probably call the API from inside a try/catch block


See also

[edit | edit source]