I've been working on a script for work. It's to use Slack with PHP. At work we use CodeIgniter (sad face) so I have to accommodate and I decided to write my script as a Library.
There isn't an issue here in that the code doesn't work, as it works fine, but I am just interested to know how I can apply method chaining when calling the library so myself and my colleagues can code cleaner when we use the library.
Here's the Library I wrote - I'm more of a programmer in-training so my OOP knowledge is limited.
<?php defined('BASEPATH') OR exit('No direct script access allowed');
include('../vendor/autoload.php');
use GuzzleHttp\Client;
class Slack {
protected $ci;
private $channel;
private $endpoint;
private $icon;
private $username;
public function __construct()
{
$this->ci =& get_instance();
$this->ci->config->load('slack');
$this->baseUri = $this->ci->config->item('base_uri');
$this->channel = $this->ci->config->item('channel');
$this->endpoint = $this->ci->config->item('endpoint');
$this->icon = $this->ci->config->item('icon');
$this->username = $this->ci->config->item('username');
$this->client = new Client([
'base_uri' => $this->baseUri
]);
}
public function getChannel()
{
return $this->channel;
}
public function setChannel($channel)
{
$this->channel = $channel;
return $this;
}
public function getEndpoint()
{
return $this->endpoint;
}
public function setEndpoint($endpoint)
{
$this->endpoint = $endpoint;
return $this;
}
public function getIcon()
{
return $this->icon;
}
public function setIcon($icon)
{
(mb_substr($icon, 0, 1) == ':')
? $this->iconType = 'icon_emoji'
: $this->iconType = 'icon_url';
$this->icon = $icon;
return $this;
}
public function getIconType()
{
return ($this->iconType) ? $this->iconType : 'icon_emoji';
}
public function getUsername()
{
return $this->username;
}
public function setUsername($username)
{
$this->username = $username;
return $this;
}
public function to($channel)
{
$this->setChannel($channel);
return $channel;
}
public function from($username)
{
$this->setUsername($username);
return $username;
}
public function icon($icon)
{
$this->setIcon($icon);
return $icon;
}
public function payload($text)
{
$payload = [
'channel' => $this->getChannel(),
$this->getIconType() => $this->getIcon(),
'link_names' => 1,
'text' => $text,
'username' => $this->getUsername(),
];
return $payload;
}
public function send($text)
{
$payload = json_encode($this->payload($text));
$this->client->post($this->getEndpoint(), [
'body' => $payload
]);
return $this;
}
}
Now I'm using this in our API, which is coded in a Controller, and this is how I am calling the methods:
<?php
// ...
$this->load->library('slack');
$this->slack->icon(':hotdog:');
$this->slack->send('Hello...');
As I said, this works fine...
I wanted though, to be able to do method chaining, like so:
<?php
// ...
$this->slack->icon(':hotdog:')->send('Hello...');
Can you tell me if this is possible and how to achieve it?
Thank you.
As i can see to archive what you want you just need to change that
public function icon($icon)
{
$this->setIcon($icon);
return $icon;
}
to that
public function icon($icon)
{
$this->setIcon($icon);
return $this;
}
and then you will be able to do what you want
$this->slack->icon(':hotdog:')->send('Hello...');
Anyway your icon method no need to return $icon you already have getIcon method
also your send method calling payload method before make request so that should work
Related
I'm making a Laravel package, which is a basic API Wrapper to practice. I want my code completely re-usable and neat, well that's the reason we learn OOP I think :P
Let me first attach my code, and I'll explain what I'm trying to achieve via comments.
// This is how I'm calling my class
Shiprocket::
withCredential('other-than-default') // this is optional
->order(203504661) // pass order id
->details() // finally fetch the details
// This is my main class it's behind a Larvel Facade Accessor
class Shiprocket
{
protected $credentials;
protected $token;
// I'm using it as a constructor to initilize with a different credentil pair.
public function withCredential($credential_id)
{
$this->credentials = config('shiprocket.credentials')[$credential_id];
$this->token = $this->getToken();
return $this;
}
public function __construct()
{
$this->credentials = config('shiprocket.credentials')[config('shiprocket.default_credentials')];
$this->token = $this->getToken();
}
public function order($order_id = null)
{
return new OrderResource($order_id);
// Here my doubt starts
// I want to return another class (OrderResource) for Order related methods
// so that we can call Order related methods like:
// Shiprocket::withCredential('my-credential')->order()->getAll()
// and those methods will also use methods & properties of this Main class
// like the token, get(), post()
}
public function shipment($shipment_id = null)
{
return new ShipmentResource($shipment_id);
// and maybe I can also have more child classes like OrderResource
// So that I can call similar methods as OrderResource for shipments like ... ->getAll()
// or ... ->status()
// but these methods won't be reusable - they'll be completely different, just sometimes
// might have same names.
}
public function getToken(): string
{
$duration = config('shiprocket.token_cache') ? config('shiprocket.token_cache_duration') : 0;
return cache()->remember("shiprocket-{$this->credentials['email']}", $duration, function () {
return Http::post("https://apiv2.shiprocket.in/v1/external/auth/login", [
'email' => $this->credentials['email'],
'password' => $this->credentials['password'],
])->json()['token'];
});
}
public function get($url, $data = null)
{
return Http::withToken($this->token)->get($url, $data)->json();
}
public function post($url, $data = null)
{
return Http::withToken($this->token)->post($url, $data)->json();
}
}
It's okay even if you don't attach any code, maybe just guide me a bit what would be the best way to achieve something like this.
The chain methods that you want to apply it's called the Builder pattern
Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.
you can learn and find snippets from here https://refactoring.guru/design-patterns/builder
back to your case, I cant agree that we need the builder pattern here, but let's try to have the small steps with your code, let's say you want to build Shiprocket object that contains the Order and the Shipment
the simple change you need is to return the Shiprocket so the code should look like this
<?php
class Shiprocket
{
protected $credentials;
protected $token;
private $order;
private $shipment;
public function withCredential($credential_id)
{
$this->credentials = config('shiprocket.credentials')[$credential_id];
$this->token = $this->getToken();
$this->order = null;
$this->shipment = null;
return $this;
}
public function __construct()
{
$this->credentials = config('shiprocket.credentials')[config('shiprocket.default_credentials')];
$this->token = $this->getToken();
$this->order = null;
$this->shipment = null;
}
public function order($order_id = null)
{
$this->order = new OrderResource($order_id);
return $this;
}
public function shipment($shipment_id = null)
{
$this->shipment = new ShipmentResource($shipment_id);
return $this;
}
public function getOrder(){
return $this->order;
}
public function getShipment(){
return $this->shipment;
}
public function getToken(): string
{
$duration = config('shiprocket.token_cache') ? config('shiprocket.token_cache_duration') : 0;
return cache()->remember("shiprocket-{$this->credentials['email']}", $duration, function () {
return Http::post("https://apiv2.shiprocket.in/v1/external/auth/login", [
'email' => $this->credentials['email'],
'password' => $this->credentials['password'],
])->json()['token'];
});
}
public function get($url, $data = null)
{
return Http::withToken($this->token)->get($url, $data)->json();
}
public function post($url, $data = null)
{
return Http::withToken($this->token)->post($url, $data)->json();
}
}
Note: the code could not be perfect when it comes to the standard and the best practice I just change it to follow your idea
I hope it's helpful
I want to be able to use an object like below, to retrieve new orders and new invoices. I feel like it is most readable, but I am having trouble writing the PHP class to work this way.
$amazon = new Amazon();
$amazon->orders('New')->get();
$amazon->invoices('New')->get();
In my PHP class, how would my get() method be able to distinguish whether to return orders or invoices?
<?php
namespace App\Vendors;
class Amazon
{
private $api_key;
public $orders;
public $invoices;
public function __construct()
{
$this->api_key = config('api.key.amazon');
}
public function orders($status = null)
{
$this->orders = 'orders123';
return $this;
}
public function invoices($status = null)
{
$this->invoices = 'invoices123';
return $this;
}
public function get()
{
// what is the best way to return order or invoice property
// when method is chained?
}
}
A couple of ways, if you want it dynamic and don't do any logic in the methods, use something like __call
<?php
class Amazon {
public $type;
public $method;
public function get()
{
// do logic
// ...
return 'Fetching: '.$this->method.' ['.$this->type.']';
}
public function __call($method, $type)
{
$this->method = $method;
$this->type = $type[0];
return $this;
}
}
$amazon = new Amazon();
echo $amazon->orders('New')->get();
echo $amazon->invoices('New')->get();
If you want to do logic in the methods, do something like:
<?php
class Amazon {
public $type;
public $method;
public function get()
{
return 'Fetching: '.$this->method.' ['.$this->type.']';
}
public function orders($type)
{
$this->method = 'orders';
$this->type = $type;
// do logic
// ...
return $this;
}
public function invoices($type)
{
$this->method = 'invoices';
$this->type = $type;
// do logic
// ...
return $this;
}
}
$amazon = new Amazon();
echo $amazon->orders('New')->get();
echo $amazon->invoices('New')->get();
As orders and invoices are set methods, I would suggest to do as follows:
public function get(array $elements)
{
$result = [];
foreach($elements as $element) {
$result[$element] = $this->$element;
}
return $result;
}
So, you can call get method as:
$amazon = new Amazon();
$amazon->orders('New')->invoices('New')->get(['orders', 'invoices']);
** You need to validate the element's availability within the get method.
I am trying to work through getting the PHP client for Google Map to work correctly.
I've downloaded a local copy of the GoogleAPI PHP Client from GitHub:https://github.com/google/google-api-php-client.
I am running PHP v5.4 on IIS8. The GoogleAPI was installed in the PHP Include folder, under GoogleAPI.
PHP works correctly with all my other scripts.
I am trying get the example to work from Maps-Engine Documentation.
<?php
ini_set('display_errors','on');
require('GoogleAPI/autoload.php');
//require_once 'GoogleAPI/src/Google/Client.php';
//require_once 'Google/Service/MapsEngine.php';
$apiKey = "API Key";
$client = new Google_Client();
$client->setApplicationName("Google-PhpMapsEngineSample/1.0");
$client->setDeveloperKey($apiKey);
$service = new Google_Service_MapsEngine($client);
$optParams = array('maxResults' => 500, 'version' => 'published');
$results = $service->tables_features->listTablesFeatures("12421761926155747447-06672618218968397709", $optParams);
print_r($results);
?>
The only changes to the code example were the API Key, load the Google Autoloader and comment out the require_once directives.
The output I receive is:
Fatal error: Class 'Google_Service_MapsEngine_MapItem' not found in C:\Program Files (x86)\PHP\v5.4\includes\GoogleAPI\src\Google\Service\MapsEngine.php on line 4702
MapsEngine:4702 extends the Google_Service_MapsEngine_MapItem class. The Google_Service_MapsEngine_MapItem class extends the Google_Model class defined in Model.php file.
Hi I had the same problem.
There is a bug in the google-api-php-client/src/Google/Service/MapsEngine.php file. The class Google_Service_MapsEngine_MapFolder which exends the Google_Service_MapsEngine_MapItem is declared before the class Google_Service_MapsEngine_MapItem is declared.
I switch the order of the 2 classes in the MapsEngine.php file and that fixed the problem. This shows the correct order for the classes.
class Google_Service_MapsEngine_MapItem extends Google_Model
{
protected $internal_gapi_mappings = array(
);
public $type;
public function setType($type)
{
$this->type = $type;
}
public function getType()
{
return $this->type;
}
}
class Google_Service_MapsEngine_MapFolder extends Google_Service_MapsEngine_MapItem
{
protected $collection_key = 'defaultViewport';
protected $internal_gapi_mappings = array(
);
protected $contentsType = 'Google_Service_MapsEngine_MapItem';
protected $contentsDataType = 'array';
public $defaultViewport;
public $expandable;
public $key;
public $name;
public $visibility;
protected function gapiInit()
{
$this->type = 'folder';
}
public function setContents($contents)
{
$this->contents = $contents;
}
public function getContents()
{
return $this->contents;
}
public function setDefaultViewport($defaultViewport)
{
$this->defaultViewport = $defaultViewport;
}
public function getDefaultViewport()
{
return $this->defaultViewport;
}
public function setExpandable($expandable)
{
$this->expandable = $expandable;
}
public function getExpandable()
{
return $this->expandable;
}
public function setKey($key)
{
$this->key = $key;
}
public function getKey()
{
return $this->key;
}
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function setVisibility($visibility)
{
$this->visibility = $visibility;
}
public function getVisibility()
{
return $this->visibility;
}
}
So, I want to create a client base URL with singleton.
This is my GuzzleClient.php which is containing the base URL
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class GuzzleClient {
public static function getClient()
{
static $client = null;
if (null === $client)
{
$client = new Client([
'base_url' => 'http://localhost:8080/task_manager/v1/',
]);
}
return $client;
}
private function __construct()
{}
}
And this one is where I should put the base url
require_once 'GuzzleClient.php';
class CardTypeAPIAccessor
{
private $client;
public function __construct($client)
{
$this->client = $client;
}
public function getCardTypes() {
$cardTypes = array();
try
{
//this is where base URL should be
$response = $client->get('admin/card/type',
['headers' => ['Authorization' => $_SESSION['login']['apiKey']]
]);
$statusCode = $response->getStatusCode();
// Check that the request is successful.
if ($statusCode == 200)
{
$error = $response->json();
foreach ($error['types'] as $type)
{
$cardType = new CardType();
$cardType->setId($type['card_type_id']);
$cardType->setCategory($type['category']);
array_push($cardTypes, $cardType);
}
}
}
}
}
I stuck with how to put the method in GuzzleClient into this code.
Thanks
I'm creating an abstract class for my webservices that send requests. The instance of guzzle is created in a singleton file:
use Guzzle\Http\Client;
class GuzzleSingleton
{
private static $_instance = null;
private static $baseUrl = '';
public static function getBaseUrl(): string
{
return self::$baseUrl;
}
private static function setBaseUrl($baseUrl): void
{
self::$baseUrl = $baseUrl;
}
public static function getInstance(string $apiUrl, ?int $apiPort, string $apiSuffix): ?Client
{
if (is_null(self::$_instance)) {
self::buildApiUrl($apiUrl, $apiPort, $apiSuffix);
self::$_instance = new Client(self::getBaseUrl());
} else {
self::buildApiUrl($apiUrl, $apiPort, $apiSuffix);
self::$_instance->setBaseUrl(self::getBaseUrl());
}
return self::$_instance;
}
private static function buildApiUrl(string $apiUrl, ?int $apiPort, string $apiSuffix): void
{
$apiDns = rtrim($apiUrl, '/');
$urlApi = (!is_null($apiPort)) ? sprintf('%s:%d', $apiDns, $apiPort) : $apiUrl;
$urlApi .= $apiSuffix;
self::setBaseUrl($urlApi);
}
}
My abstract class:
abstract class AbstractWebservice
{
public function __construct(string $apiUrl, ?int $apiPort, ?string $apiSuffixUrl, Session $session)
{
$this->setApiUrl($apiUrl);
$this->setApiPort($apiPort);
$this->setApiSuffixUrl($apiSuffixUrl);
$this->session = $session;
$this->buildFactory();
}
public function buildFactory(): void
{
$this->guzzleWs = GuzzleSingleton::getInstance($this->getApiUrl(), $this->getApiPort(), $this->getApiSuffixUrl());
}
public function sendRequest(string $endpoint, ?array $body, ?array $options, string $method)
{
$request = $this->guzzleWs->createRequest($method, $endPoint, self::$headers, $body, $options);
return $request->send();
}
}
inharit GuzzleClient class into CardTypeAPIAccessor, and check if $client is notinstanceof GuzzleClient then assign access object into $this->client
class CardTypeAPIAccessor extends GuzzleClient
{
private $client;
public function __construct($client)
{
if($client instanceof GuzzleClient){
$this->client = $client
}else{
$this->client = parent::getClient();
}
}
}
I'm writing a wrapper (Adapter pattern) for a number of REST calls. I'm trying to decide how much my response object should be able to do.
class Job
{
private $xml;
public function __construct(SimpleXMLElement $xml)
{
$this->xml = $xml;
}
public function getName()
{
return $this->xml->Name;
}
public function getId()
{
return $this->xml->ID;
}
...
...
...
}
class RESTManager
{
private $client;
public function __construct(Guzzle $client)
{
$this->client = $client
}
public function getJobByName($name)
{
$job = $this->client->get('/query/job/' . $name);
return new Job($job->asXMLObj())
}
}
So I have something similar to the above. Once I have the Job object I will want to get some additional information such as, a list of items in the Job. This requires another server call
Should I ask the Job what the items are
class Job {
private $manager;
...
...
public function addManager(RESTManager $manager)
{
$this->manager = $manager;
}
public function getItems()
{
return $this->manager->getJobItems($this);
}
}
or
ask the RESTManager directly?
class RESTManager {
...
...
public function getJobItems(Job $job)
{
return $this->client->get('/job/' . $job->getId() . '/items');
}
}