I want to consume a Rest API in Laravel (an MVC framework) but I resort to use __call and was wonder if there is a better design pattern for this.
I know this a bad choice and I'm looking for an alternative pattern but here is my Repository class:
namespace App\Repositories;
use App\Models\OnlinePayment;
use App\Models\Order;
use App\Models\Transaction;
use App\Models\User;
use GuzzleHttp\Client;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use SoapClient;
class Bank
{
protected $http;
protected $user;
public function __construct()
{
$this->http = new Client;
}
protected function index()
{
$url = config('Bank.url') . '/v2/quantum/users/' . $this->user->national_id . '/report';
$data = [
'user_gender' => $this->user->gender ?? 1,
'user_name' => $this->user->name,
'user_family' => $this->user->family ?? 'خالی',
'user_mobile' => $this->user->mobile,
'user_type' => $this->user->type->name,
];
$options = $this->options($data);
$res = $this->http->request('GET', $url, $options);
$response = json_decode($res->getBody(), true);
return $response;
}
protected function indexData($request)
{
$url = config('Bank.url') . '/v2/quantum/users/' . $this->user->national_id . '/customers';
$options = $this->options($request->all());
$res = $this->http->request('GET', $url, $options);
$response = response()->json(json_decode($res->getBody(), true), $res->getStatusCode());
return $response;
}
protected function show($national_id)
{
$url = config('Bank.url') . '/v2/quantum/users/' . $this->user->national_id . '/customers/' . $national_id;
$options = $this->options([]);
$res = $this->http->request('GET', $url, $options);
if ($res->getStatusCode() == 404) {
abort(404);
}
$response = json_decode($res->getBody(), true);
return $response;
}
protected function store($request)
{
$http = new Client;
$url = config('Bank.url') . '/v2/quantum/users/' . $this->user->national_id . '/customers';
$this->user = auth()->user();
$data = array_merge(
[
'customer_national_id' => $request->national_id,
'customer_gender' => $request->gender,
'customer_name' => $request->name,
'customer_family' => $request->family,
'customer_phone' => $request->phone,
'customer_mobile' => $request->mobile,
'customer_city_id' => $request->city_id,
], [
'user_name' => $this->user->nanfig() is a hidden dependency. The settings should also be passed via the construcme,
'user_family' => $this->user->family ?? 'خالی',
'user_mobile' => $this->user->mobile,
'user_type' => $this->user->type->name,
'user_gender' => $this->user->gender ?? 1,
]
);
$res = $http->request('POST', $url, [
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . config('Bank.token'),
],
'json' => $data,
'http_errors' => false
]);
if (! in_array($res->getStatusCode(), [200, 422])) {
$error = ValidationException::withMessages([
'name' => 'خطای ' . $res->getStatusCode() . ' در تعویض کالا'
]);
throw $error;
}
$response = response()->json(json_decode($res->getBody(), true), $res->getStatusCode());
return $response;
}
protected function options($data)
{
$options = [
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . config('Bank.token'),
],
'json' => $data,
'http_errors' => false
];
return $options;
}
public function __call($method, $arguments) {
if (method_exists($this, $method)) {
if (! isset($arguments[0]) || ! $arguments[0] instanceof User) {
$this->user = auth()->user();
} else {
$this->user = $arguments[0];
unset($arguments[0]);
}
return call_user_func_array(array($this, $method), $arguments);
}
}
}
then create an instance of it in controller constructor:
public function __construct()
{
$this->Bank = new Bank();
}
and use it in controller like this:
$response = $this->Bank->indexData($user, $request);
or this:
$response = $this->Bank->indexData($request);
I think the shown class is not a Repository class because a Repository is only responsible for reading and writing the date from a data source. Your class does too much and violates all basic MVC principles.
Some thinks I would fix:
A repository is not responsible for creating the response view data (like JSON)
A repository is not responsible for creating a response object
A repository is independent of the request/response
The method name index makes no sense because a repository is not a Controller action. Don't mix the model layer with the controller layer.
config() is a hidden dependency. The settings should also be passed via the constructor.
Instead use better separation:
Create a class BankApiClient
Dont use magic methods like __call
Instead use public methods like: getUserByNationalId(int $nationalId): UserData
and so on...
Let the controller action create / render the json response with the results of the BankApiClient.
__call is an magic method of php which allow to execute protected method outside of the object instance, this is a rupture of the class visibility.
If you want to call a method from outside it must be public
public function __construct()
{
$this->bank = new Bank()
}
Use Auto injection of the dependency
public function __construct(Bank $bank)
{
$this->bank = $bank;
}
Related
I'm trying to set a sensorId property that will be used by every method in my feature test. The problem is that it is not persisting between tests.
Here is my test class:
class LogApiPostTest extends TestCase
{
public $sensorId;
public function setUp(): void
{
parent::setUp();
$this->sensorId = 'SEN' . rand(10000000, 999999999);
}
public function test_creates_sensor()
{
$response = $this->post('/api/logs', $this->data([
//
]));
$response->assertStatus(200);
$response->assertSeeText('Sensor created');
}
public function test_updates_sensor()
{
$response = $this->post('/api/logs', $this->data([
//
]));
$response->assertStatus(200);
$response->assertSeeText('Sensor updated');
}
public function test_creates_car()
{
$response = $this->post('/api/logs', $this->data([
'CarID' => 'B',
]));
$response->assertStatus(200);
$response->assertSeeText('Car created');
}
public function test_updates_car()
{
$response = $this->post('/api/logs', $this->data([
'CarID' => 'B',
]));
$response->assertStatus(200);
$response->assertSeeText('Car updated');
}
public function test_is_unauthorized()
{
$response = $this->post('/api/logs', $this->data([
'token_id' => 'thisShouldntWork',
]));
$response->assertUnauthorized();
}
public function data($merge = [])
{
return array_merge([
'token_id' => config('api.token'),
'sensorID' => $this->sensorId,
'CarID' => 'A',
'LogType' => 'CarLog',
'Time' => time(),
'DrctnFlr' => '-02',
'MCSS' => 'STB',
'OpMode' => 'INA',
'DoorStats' => '][**',
'DoorCmd' => 'C6:C1>',
'OCSS' => 'GTN02',
'Load' => '002%',
], $merge);
}
}
I just want the sensorId property to persist for all tests in this class.
When there is a dependency between tests, you must use the #depends annotation.
This allows to ensure the execution order of tests is correct and also to pass values between tests. Instead of making sensorId to persist, just pass it from test to test.
https://phpunit.readthedocs.io/en/9.5/writing-tests-for-phpunit.html#test-dependencies
I'm doing a login with passport (in API) I'm trying to get the tokens generated by the authentication server. However, I can't add extra parameters to the request which is an instance of FormRequest.
On the other hand, if I change my request to an instance of Request, it works.
So, my question how I can add parameters to my query $loginRequest (which is instance of FormRequest)
$loginRequest->request->add($params);
Here my code:
class AuthController extends Controller
{
use ThrottlesLogins;
public function store(LoginRequest $loginRequest)
{
$loginRequest->validated();
if ($this->hasTooManyLoginAttempts($loginRequest)) {
$this->fireLockoutEvent($loginRequest);
return $this->sendLockoutResponse($loginRequest);
}
if (Auth::attempt($this->credentials($loginRequest))){
$client = $this->getClient($loginRequest->name);
$params = [
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret,
'username' => $loginRequest->email,
'password' => $loginRequest->password,
'scopes' => 'fd',
];
$loginRequest->request->add($params);
$req = Request::create('oauth/token', 'POST');
$response = Route::dispatch($req)->getContent();
return $response;
}
$this->incrementLoginAttempts($loginRequest);
$this->sendFailedLoginResponse($loginRequest);
}
}
To append properties to an instance of FormRequest you can use the merge() method.
public function store(LoginRequest $loginRequest) {
$params = [
'foo' => 'bar',
];
$loginRequest->merge($params);
}
I am developing my unit tests for an API created in Symfony4
Reading the Guzzle documentation I generated the following code:
File SecurityControllerTest.php
$client = new Client([
'base_uri' => 'http://localhost/sacrepad/sacrepad-api/public/index.php/',
'timeout' => 2.0,
]);
$data = array();
$data['email'] = 'admin#admin.com';
$data['password'] = '12345678';
$data2 = array();
$data2['json'] = $data;
$formData = json_encode($data);
$response = $client->request('POST', 'login', [
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
'form_params' => [
'json' => $formData,
]
]);
$body = json_decode($response->getBody(), true);
File SecurityController.php
/**
* #Route("/login", name="login", methods={"POST"})
*/
public function login(Request $request,Helpers $helpers,ValidatorInterface $validator, JwtAuth $jwtauth) {
$data = array(
'status' => 'error',
'code' => 400,
'msg' => 'data not received'
);
$json = $request->request->get('json');
$params = json_decode($json);
}
When I run the tests with the phpunit command, I get the following error:
1) App\Tests\SecurityControllerTest::testAuth GuzzleHttp\Exception\ServerException: Server error: `POST http://localhost/sacrepad/sacrepad-api/public/index.php/login` resulted in a `500 Internal Server Error` response:
If I change the name of the request:
$json = $request->request->get('json2');
It works and it returns me the following:
array(3) {
["status"]=>
string(5) "error"
["code"]=>
int(400)
["msg"]=>
string(18) "data not received"
}
Any ideas on how to make it work and send the parameters?
i build a class for working with guzzle
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class Api
{
protected $client;
protected $url;
public function __construct()
{
$this->client = new Client([
'verify'=>false
]);
$this->url = 'http://localhost/sacrepad/sacrepad-api/public/';
}
public function get($endpoint, $params = [], $headers = [])
{
$response = $this->sendRequest(
'GET',
$this->url . $endpoint,
$params,
$headers
);
return $response;
}
public function post($endpoint, $params = [], $headers = [])
{
$response = $this->sendRequest(
'POST',
$this->url . $endpoint,
$params,
$headers
);
return $response;
}
public function sendRequest($type, $url, $params = [], $headers = [])
{
if ($type == 'GET') {
$data = [
'query' => $params
];
} elseif ($type == 'FILE') {
$type = 'POST';
$data = [
'multipart' => $params // TODO implements later
];
} else {
$data = [
'json' => $params
];
}
if (!empty($headers)) {
$data['headers'] = $headers;
}
$data['headers']['X-REAL-IP'] = $_SERVER['REMOTE_ADDR'];
$data['headers']['User-Agent'] = $_SERVER['HTTP_USER_AGENT'];;
$data['headers']['X-Platform'] = 'web';
try {
$response = $this->client->request(
$type,
$url,
$data
);
if (in_array($response->getStatusCode(), ['200', '403', '404'])) {
return json_decode($response->getBody());
}
return false;
} catch (RequestException $re) {
if (in_array($re->getResponse()->getStatusCode(), ['403', '404', '422'])) {
return json_decode($re->getResponse()->getBody());
}
return json_decode($re->getResponse()->getBody());
} catch (Exception $e) {
return false;
}
}
}
when i want to send a post request it would be like this
$response = (new Api())->post('index.php/',[
'email'=> 'admin#admin.com',
'password' => '123456'
]);
now it will send a post request to index.php and send email and password data i hope it would be helpful
I'm using Dingo API to create an API in Laravel 5.2 and have a controller returning data with
return $this->response->paginator($rows, new SymptomTransformer, ['user_id' => $user_id]);
However, I don't know how to retrieve user_id value in the SymptomTransformer! Tried many different ways and tried looking into the class but I'm relatively new to both Laravel and OOP so if anyone can point me to the right direction, it'd be greatly appreciated.
Below is my transformer class.
class SymptomTransformer extends TransformerAbstract
{
public function transform(Symptom $row)
{
// need to get user_id here
return [
'id' => $row->id,
'name' => $row->name,
'next_type' => $next,
'allow' => $allow
];
}
}
You can pass extra parameter to transformer constructor.
class SymptomTransformer extends TransformerAbstract
{
protected $extra;
public function __construct($extra) {
$this->extra = $exta;
}
public function transform(Symptom $row)
{
// need to get user_id here
dd($this->extra);
return [
'id' => $row->id,
'name' => $row->name,
'next_type' => $next,
'allow' => $allow
];
}
}
And call like
return $this->response->paginator($rows, new SymptomTransformer(['user_id' => $user_id]));
You can set extra param via setter.
class SymptomTransformer extends TransformerAbstract
{
public function transform(Symptom $row)
{
// need to get user_id here
dd($this->test_param);
return [
'id' => $row->id,
'name' => $row->name,
'next_type' => $next,
'allow' => $allow
];
}
public function setTestParam($test_param)
{
$this->test_param = $test_param;
}
}
And then:
$symptomTransformer = new SymptomTransformer;
$symptomTransformer->setTestParam('something');
return $this->response->paginator($rows, $symptomTransformer);
If you are using Dependency Injection, then you need to pass params afterwards.
This is my strategy:
<?php
namespace App\Traits;
trait TransformerParams {
private $params;
public function addParam() {
$args = func_get_args();
if(is_array($args[0]))
{
$this->params = $args[0];
} else {
$this->params[$args[0]] = $args[1];
}
}
}
Then you implement the trait in your transformer:
<?php
namespace App\Transformers;
use App\Traits\TransformerParams;
use App\User;
use League\Fractal\TransformerAbstract;
class UserTransformer extends TransformerAbstract
{
use TransformerParams;
public function transform(User $user)
{
return array_merge([
'id' => (int) $user->id,
'username' => $user->username,
'email' => $user->email,
'role' => $user->roles[0],
'image' => $user->image
], $this->params); // in real world, you'd not be using array_merge
}
}
So, in your Controller, just do this:
public function index(Request $request, UserTransformer $transformer)
{
$transformer->addParam('has_extra_param', ':D');
// ... rest of the code
}
Basically, the trait is a bag for extra params.
In the TokenRepository you can see 3 similar methods. It create new entry to the tokens table but each method has different fields.
How can I refactor this? Should I merge 3 methods into 1 method or should I use strategy pattern?
TokenRepository Class:
class TokenRepository
{
public function createTokenDigitalOcean(User $user, $name, $accessToken, $refreshToken = null)
{
return $user->tokens()->create([
'name' => $name,
'provider' => 'digital_ocean',
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
]);
}
public function createTokenLinode(User $user, $name, $key)
{
return $user->tokens()->create([
'name' => $name,
'provider' => 'linode',
'linode_key' => $key,
]);
}
public function createTokenAws(User $user, $name, $key, $secret)
{
return $user->tokens()->create([
'name' => $name,
'provider' => 'aws',
'aws_key' => $key,
'aws_secret' => $secret,
]);
}
}
I have 3 classes like DigitalOceanProvider, LinodeProvider and AwsProvider. For example of using LinodeProvider and AwsProvider class.
class LinodeProvider
{
public function callback()
{
$this->tokenRepo->createTokenLinode($user, $name, $key);
}
}
class AwsProvider
{
public function callback()
{
$this->tokenRepo->createTokenAws($user, $name, $key, $secret);
}
}
This may be a bit overkill, but in order to make life a bit easier in the future, you could create separate implementations of each that extend an abstract class. This way you can unify and define the interface and easily add new token types.
<?php namespace Foo\Tokens;
abstract class Token
{
protected $name = '';
protected $key = '';
protected $provider = '';
public function __construct($name, $key)
{
$this->name = $name;
$this->key = $key;
}
public function data()
{
return [
'name' => $this->name,
'provider' => $this->provider,
'token' => $this->key
];
}
}
Next, we create our Digital Ocean token class. This class can either use the default implementation or redefine it.
<?php namespace Foo\Tokens;
use Foo\Tokens\Token;
class DigitalOceanToken extends Token
{
protected $provider = 'digital_ocean';
public function __construct($name, $key, $refreshToken = null)
{
parent::__construct($name, $key);
$this->refreshToken = $refreshToken;
}
public function data()
{
return [
'name' => $this->name,
'provider' => $this->provider,
'key' => $this->key,
'refreshToken' => $this->refreshToken
];
}
}
The TokenRepository now merely cares about attaching a given token to a user.
<?php namespace Foo;
use User;
use Foo\Tokens\Token;
class TokenRepository
{
public function createToken(User $user, Token $token)
{
return $user->tokens()->create(
$token->data()
);
}
}
And your service providers are as simple as...
<?php
use Foo\Tokens\AwsToken;
class AwsProvider
{
public function callback()
{
$this->tokenRepo->createToken(
$user, new AwsToken($name, $key, $secret)
);
}
}
This isn't working code, as I've not attempted to run it however it's just another idea of how you can organize and assign responsibility. Hope it helps, and welcome feedback from others.
According to me you should implement it like this:
class TokenRepository
{
public function createTokenForVendor(User $user, $inputs)
{
return $user->tokens()->create($inputs);
}
}
and inside your callback:
class VendorProvider
{
public function callback()
{
switch($tokenType) {
case 'DigitalOcean':
$inputs = [
'name' => $name,
'provider' => 'digital_ocean',
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
];
break;
case 'Linode':
$inputs = [
'name' => $name,
'provider' => 'linode',
'linode_key' => $key,
];
break;
case 'Aws':
$inputs = [
'name' => $name,
'provider' => 'aws',
'aws_key' => $key,
'aws_secret' => $secret,
];
break;
}
$this->tokenRepo->createTokenForVendor($user, $inputs);
}
}
Hoping you should do some code structure revamp.
Hope this helps!