How to implement service contract for a custom module in Magento 2?
I would like to give a bit more detail in addition to the excellent answer of @ryanF.
I would like to sum up the reasons to add a repository for custom entities, give examples how to do so, and also explain how to expose those repository methods as part of the Web API.
Disclaimer: I'm only describing a pragmatic approach how to do this for third party modules - the core teams have their own standards which they follow (or not).
In general, the purpose of a repository is to hide the storage related logic.
A client of a repository should not care whether the returned entity is held in memory in an array, is retrieved from a MySQL database, fetched from a remote API or from a file.
I assume the Magento core team did this so they are able to change or replace the ORM in future. In Magento the ORM currently consists of the Models, Resource Models and Collections.
If a third party module use only the repositories, Magento can change how and where data is stored, and the module will continue to work, despite these deep changes.
Repositories generally have methods like findById()
, findByName()
, put()
or remove()
.
In Magento these commonly are called getbyId()
, save()
and delete()
, not even pretending they are doing anything else but CRUD DB operations.
Magento 2 repository methods can easily be exposed as API resources, making them valuable for integrations with third party systems or headless Magento instances.
"Should I add a repository for my custom entity?".
As always, the answer is
"It depends".
To make a long story short, if your entities will be used by other modules, then yes, you probably want to add a repository.
There is another factor that comes into count here: in Magento 2, repositories can easily be exposed as Web API - that is REST and SOAP - resources.
If that is interesting to you because of third party system integrations or a headless Magento setup, then again, yes, you probably want to add a repository for your entity.
How do I add a repository for my custom entity?
Lets assume you want to expose your entity as part of the REST API. If that is not true, you can skip the upcoming part on creating the interfaces and go straight to "Create the repository and data model implementation" below.
Create the repository and data model interfaces
Create the folders Api/Data/
in your module. This is just convention, you could use a different location, but you should not.
The repository goes into the Api/
folder. The Data/
subdirectory is for later.
In Api/
, create a PHP interface with the methods you want to expose. According to Magento 2 conventions all interface names end in the suffix Interface
.
For example, for a Hamburger
entity, I would create the interface Api/HamburgerRepositoryInterface
.
Magento 2 repositories are part of the domain logic of a module. That means, there is no fixed set of methods a repository has to implement.
It depends entirely on the purpose of the module.
However, in practice all repositories are quite similar. They are wrappers for CRUD functionality.
Most have the methods getById
, save
, delete
and getList
.
There may be more, for example the CustomerRepository
has a method get
, which fetches a customer by email, whereby getById
is used to retrieve a customer by entity ID.
Here is an example repository interface for a hamburger entity:
<?php
namespace VinaiKopp\Kitchen\Api;
use Magento\Framework\Api\SearchCriteriaInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerInterface;
interface HamburgerRepositoryInterface
{
/**
* @param int $id
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getById($id);
/**
* @param \VinaiKopp\Kitchen\Api\Data\HamburgerInterface $hamburger
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
*/
public function save(HamburgerInterface $hamburger);
/**
* @param \VinaiKopp\Kitchen\Api\Data\HamburgerInterface $hamburger
* @return void
*/
public function delete(HamburgerInterface $hamburger);
/**
* @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface
*/
public function getList(SearchCriteriaInterface $searchCriteria);
}
Important! Here be timesinks!
There are a few gotchas here that are hard to debug if you get them wrong:
- DO NOT use PHP7 scalar argument types or return types if you want to hook this into the REST API!
- Add PHPDoc annotations for all arguments and the return type to all methods!
- Use Fully Qualified Class Names in the PHPDoc block!
The annotations are parsed by the Magento Framework to determine how to convert data to and from JSON or XML.
Class imports (that is, use
statements) are not applied!
Every method has to have an annotation with any argument types and the return type. Even if a method takes no arguments and returns nothing, it has to have the annotation:
/**
* @return void
*/
Scalar types (string
, int
, float
and bool
) also have to be specified, both for arguments and as a return value.
Note that in the example above, the annotations for methods that return objects are specified as interfaces, too.
The return type interfaces are all in the Api\Data
namespace/directory.
This is to indicate that they do not contain any business logic. They are simply bags of data.
We have to create these interfaces next.
I think Magento calls these interfaces "data models", a name I don't like at all.
This type of class is commonly known as a Data Transfer Object, or DTO.
These DTO classes only have getters and setters for all their properties.
The reason I prefer to use DTO over data model is that it is less easy to confuse with the ORM data models, resource models or view models... too many things are models in Magento already.
The same restrictions in regards to PHP7 typing that apply to repositories also apply to DTOs.
Also, every method has to have an annotation with all argument types and the return type.
<?php
namespace VinaiKopp\Kitchen\Api\Data;
use Magento\Framework\Api\ExtensibleDataInterface;
interface HamburgerInterface extends ExtensibleDataInterface
{
/**
* @return int
*/
public function getId();
/**
* @param int $id
* @return void
*/
public function setId($id);
/**
* @return string
*/
public function getName();
/**
* @param string $name
* @return void
*/
public function setName($name);
/**
* @return \VinaiKopp\Kitchen\Api\Data\IngredientInterface[]
*/
public function getIngredients();
/**
* @param \VinaiKopp\Kitchen\Api\Data\IngredientInterface[] $ingredients
* @return void
*/
public function setIngredients(array $ingredients);
/**
* @return string[]
*/
public function getImageUrls();
/**
* @param string[] $urls
* @return void
*/
public function setImageUrls(array $urls);
/**
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface|null
*/
public function getExtensionAttributes();
/**
* @param \VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface $extensionAttributes
* @return void
*/
public function setExtensionAttributes(HamburgerExtensionInterface $extensionAttributes);
}
If a method retrieves or returns an array, the type of the items in the array has to be specified in the PHPDoc annotation, followed by an opening and closing square bracket []
.
This is true for both scalar values (e.g. int[]
) as well as objects (e.g. IngredientInterface[]
).
Note that I'm using an Api\Data\IngredientInterface
as an example for a method returning an array of objects, I'll won't add the code of the ingredients to this post tough.
In the example above the HamburgerInterface
extends the ExtensibleDataInterface
.
Technically this is only required if you want other modules to be able to add attributes to your entity.
If so, you also need to add another getter/setter pair, by convention called getExtensionAttributes()
and setExtensionAttributes()
.
The naming of the return type of this method is very important!
The Magento 2 framework will generate the interface, the implementation, and the factory for the implementation if you name them just right. The details of these mechanics are out of scope of this post though.
Just know, if the interface of the object you want to make extensible is called \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
, then the extension attributes type has to be \VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface
. So the word Extension
has to be inserted after the entity name, right before the Interface
suffix.
If you do not want your entity to be extensible, then the DTO interface does not have to extend any other interface, and the getExtensionAttributes()
and setExtensionAttributes()
methods can be omitted.
Enough about the DTO interface for now, time to return to the repository interface.
The getList() return type SearchResultsThe repository method getList
returns yet another type, that is, a SearchResultsInterface
instance.
The method getList
could of course just return an array of objects matching the specified SearchCriteria
,
but returning a SearchResults
instance allows adding some useful meta data to the returned values.
You can see how that works below in the repository getList()
method implementation.
Here is the example hamburger search result interface:
<?php
namespace VinaiKopp\Kitchen\Api\Data;
use Magento\Framework\Api\SearchResultsInterface;
interface HamburgerSearchResultInterface extends SearchResultsInterface
{
/**
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerInterface[]
*/
public function getItems();
/**
* @param \VinaiKopp\Kitchen\Api\Data\HamburgerInterface[] $items
* @return void
*/
public function setItems(array $items);
}
All this interface does is it overrides the types for the two methods getItems()
and setItems()
of the parent interface.
We now have the following interfaces:
\VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface
\VinaiKopp\Kitchen\Api\Data\HamburgerInterface
\VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface
The repository extends nothing,
the HamburgerInterface
extends the \Magento\Framework\Api\ExtensibleDataInterface
,
and the HamburgerSearchResultInterface
extends the \Magento\Framework\Api\SearchResultsInterface
.
Create the repository and data model implementations
The next step is to create the implementations of the three interfaces.
The RepositoryIn essence, the repository uses the ORM to do it's job.
The getById()
, save()
and delete()
methods are quite straight forward.
The HamburgerFactory
is injected into the repository as a constructor argument, as can be seen a bit further below.
public function getById($id)
{
$hamburger = $this->hamburgerFactory->create();
$hamburger->getResource()->load($hamburger, $id);
if (! $hamburger->getId()) {
throw new NoSuchEntityException(__('Unable to find hamburger with ID "%1"', $id));
}
return $hamburger;
}
public function save(HamburgerInterface $hamburger)
{
$hamburger->getResource()->save($hamburger);
return $hamburger;
}
public function delete(HamburgerInterface $hamburger)
{
$hamburger->getResource()->delete($hamburger);
}
Now to the most interesting part of a repository, the getList()
method.
The getList()
method has to translate the SerachCriteria
conditions into method calls on the collection.
The tricky part of that is getting the AND
and OR
conditions for the filters right, especially since the syntax for setting the conditions on the collection is different depending on whether it is an EAV or a flat table entity.
In most cases, getList()
can be implemented as illustrated in the example below.
<?php
namespace VinaiKopp\Kitchen\Model;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Exception\NoSuchEntityException;
use VinaiKopp\Kitchen\Api\Data\HamburgerInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterfaceFactory;
use VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface;
use VinaiKopp\Kitchen\Model\ResourceModel\Hamburger\CollectionFactory as HamburgerCollectionFactory;
use VinaiKopp\Kitchen\Model\ResourceModel\Hamburger\Collection;
class HamburgerRepository implements HamburgerRepositoryInterface
{
/**
* @var HamburgerFactory
*/
private $hamburgerFactory;
/**
* @var HamburgerCollectionFactory
*/
private $hamburgerCollectionFactory;
/**
* @var HamburgerSearchResultInterfaceFactory
*/
private $searchResultFactory;
public function __construct(
HamburgerFactory $hamburgerFactory,
HamburgerCollectionFactory $hamburgerCollectionFactory,
HamburgerSearchResultInterfaceFactory $hamburgerSearchResultInterfaceFactory
) {
$this->hamburgerFactory = $hamburgerFactory;
$this->hamburgerCollectionFactory = $hamburgerCollectionFactory;
$this->searchResultFactory = $hamburgerSearchResultInterfaceFactory;
}
// ... getById, save and delete methods listed above ...
public function getList(SearchCriteriaInterface $searchCriteria)
{
$collection = $this->collectionFactory->create();
$this->addFiltersToCollection($searchCriteria, $collection);
$this->addSortOrdersToCollection($searchCriteria, $collection);
$this->addPagingToCollection($searchCriteria, $collection);
$collection->load();
return $this->buildSearchResult($searchCriteria, $collection);
}
private function addFiltersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
{
foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
$fields = $conditions = [];
foreach ($filterGroup->getFilters() as $filter) {
$fields[] = $filter->getField();
$conditions[] = [$filter->getConditionType() => $filter->getValue()];
}
$collection->addFieldToFilter($fields, $conditions);
}
}
private function addSortOrdersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
{
foreach ((array) $searchCriteria->getSortOrders() as $sortOrder) {
$direction = $sortOrder->getDirection() == SortOrder::SORT_ASC ? 'asc' : 'desc';
$collection->addOrder($sortOrder->getField(), $direction);
}
}
private function addPagingToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
{
$collection->setPageSize($searchCriteria->getPageSize());
$collection->setCurPage($searchCriteria->getCurrentPage());
}
private function buildSearchResult(SearchCriteriaInterface $searchCriteria, Collection $collection)
{
$searchResults = $this->searchResultFactory->create();
$searchResults->setSearchCriteria($searchCriteria);
$searchResults->setItems($collection->getItems());
$searchResults->setTotalCount($collection->getSize());
return $searchResults;
}
}
Filters within a FilterGroup
must be combined using an OR operator.
Separate filter groups are combined using the logical AND operator.
Phew
This was the biggest bit of work. The other interface implementations are simpler.
Magento originally intended developers to implement the DTO as separate classes, distinct from the entity model.
The core team only did this for the customer module though (\Magento\Customer\Api\Data\CustomerInterface
is implemented by \Magento\Customer\Model\Data\Customer
, not \Magento\Customer\Model\Customer
).
In all other cases the entity model implements the DTO interface (for example \Magento\Catalog\Api\Data\ProductInterface
is implemented by \Magento\Catalog\Model\Product
).
I've asked members of the core team about this at conferences, but I didn't get a clear response what is to be considered good practice.
My impression is that this recommendation has been abandoned. It would be nice to get an official statement on this though.
For now I've made the pragmatic decision to use the model as the DTO interface implementation. If you feel it is cleaner to use a separate data model, feel free to do so. Both approaches work fine in practice.
If the DTO inteface extends the Magento\Framework\Api\ExtensibleDataInterface
, the model has to extend Magento\Framework\Model\AbstractExtensibleModel
.
If you don't care about the extensibility, the model can simply continue to extend the ORM model base class Magento\Framework\Model\AbstractModel
.
Since the example HamburgerInterface
extends the ExtensibleDataInterface
the hamburger model extends the AbstractExtensibleModel
, as can be seen here:
<?php
namespace VinaiKopp\Kitchen\Model;
use Magento\Framework\Model\AbstractExtensibleModel;
use VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerInterface;
class Hamburger extends AbstractExtensibleModel implements HamburgerInterface
{
const NAME = 'name';
const INGREDIENTS = 'ingredients';
const IMAGE_URLS = 'image_urls';
protected function _construct()
{
$this->_init(ResourceModel\Hamburger::class);
}
public function getName()
{
return $this->_getData(self::NAME);
}
public function setName($name)
{
$this->setData(self::NAME, $name);
}
public function getIngredients()
{
return $this->_getData(self::INGREDIENTS);
}
public function setIngredients(array $ingredients)
{
$this->setData(self::INGREDIENTS, $ingredients);
}
public function getImageUrls()
{
$this->_getData(self::IMAGE_URLS);
}
public function setImageUrls(array $urls)
{
$this->setData(self::IMAGE_URLS, $urls);
}
public function getExtensionAttributes()
{
return $this->_getExtensionAttributes();
}
public function setExtensionAttributes(HamburgerExtensionInterface $extensionAttributes)
{
$this->_setExtensionAttributes($extensionAttributes);
}
}
Extracting the property names into constants allows to keep them in one place. They can be used by the getter/setter pair and also by the Setup script that creates the database table. Otherwise there is no benefit in extracting them into constants.
The SearchResultThe SearchResultsInterface
is the simplest of the three interfaces to implement, since it can inherit all of it's functionality from a framework class.
<?php
namespace VinaiKopp\Kitchen\Model;
use Magento\Framework\Api\SearchResults;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface;
class HamburgerSearchResult extends SearchResults implements HamburgerSearchResultInterface
{
}
Configure the ObjectManager preferences
Even though the implementations are complete, we still can't use the interfaces as dependencies of other classes, since the Magento Framework object manager does not know what implementations to use. We need to add an etc/di.xml
configuration for with the preferences.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" type="VinaiKopp\Kitchen\Model\HamburgerRepository"/>
<preference for="VinaiKopp\Kitchen\Api\Data\HamburgerInterface" type="VinaiKopp\Kitchen\Model\Hamburger"/>
<preference for="VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface" type="VinaiKopp\Kitchen\Model\HamburgerSearchResult"/>
</config>
How can the repository be exposed as an API resource?
This part is really simple, it's the reward for going through all the work creating the interfaces, the implementations and wiring them together.
All we need to do is create an etc/webapi.xml
file.
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route method="GET" url="/V1/vinaikopp_hamburgers/:id">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="getById"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
<route method="GET" url="/V1/vinaikopp_hamburgers">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="getList"/>
<resources>
<resource ref="anonymouns"/>
</resources>
</route>
<route method="POST" url="/V1/vinaikopp_hamburgers">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="save"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
<route method="PUT" url="/V1/vinaikopp_hamburgers">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="save"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
<route method="DELETE" url="/V1/vinaikopp_hamburgers">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="delete"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
</routes>
Note that this configuration not only enables the use of the repository as REST endpoints, it also exposes the methods as part of the SOAP API.
In the first example route, <route method="GET" url="/V1/vinaikopp_hamburgers/:id">
, the placeholder :id
has to match the name of the argument to the mapped method, public function getById($id)
.
The two names have to match, for example /V1/vinaikopp_hamburgers/:hamburgerId
would not work, since the method argument variable name is $id
.
For this example I've set the accessability to <resource ref="anonymous"/>
. This means the resource is exposed publically without any restriction!
To make a resource only available to a logged in customer, use <resource ref="self"/>
. In this case the special word me
in the resource endpoint URL will be used to populate an argument variable $id
with the ID of the currently logged in customer.
Have a look at the Magento Customer etc/webapi.xml
and CustomerRepositoryInterface
if you need that.
Finally, the <resources>
can also be used to restrict access to a resource to an admin user account. To do this set the <resource>
ref to an identifier defined in an etc/acl.xml
file.
For example, <resource ref="Magento_Customer::manage"/>
would restrict access to any admin account who is privileged to manage customers.
An example API query using curl could look like this:
$ curl -X GET http://example.com/rest/V1/vinaikopp_hamburgers/123
Note: writing this started as an answer to https://github.com/astorm/pestle/issues/195
Check out pestle, buy Commercebug and become a patreon of @alanstorm
@Raphael at Digital Pianism:
Please refer to the following sample module structure:
app/
├ code/
| ├ Namespace/
| | ├ Custom/
| | | ├ Api/
| | | | ├ CustomRepositoryInterface.php
| | | | ├ Data/
| | | | | ├ CustomInterface.php
| | | | | ├ CustomSearchResultsInterface.php
| | | ├ etc/
| | | | ├ di.xml
| | | | ├ module.xml
| | | ├ Model/
| | | | ├ Custom.php
| | | | ├ CustomRepository.php
| | | | ├ ResourceModel/
| | | | | ├ Custom.php
Create repository interface (Service Contract)
Namespace/Custom/Api/CustomRepositoryInterface.php
: http://codepad.org/WognSKnHCreate SearchResultsInterface
Namespace/Custom/Api/Data/CustomSearchResultsInterface.php
: http://codepad.org/zcbi8X4ZCreate CustomInterface (Data Container)
Namespace/Custom/Api/Data/CustomInterface.php
: http://codepad.org/Ze53eT4oCreate CustomRepository (Concrete Repository )
Namespace/Custom/Model/CustomRepository.php
: http://codepad.org/KNt5QAGZ
This is where the "magic" happens. Through constructor DI, you pass in the resource model/collection factory for your custom module; Regarding the save CRUD method in this Repository, due to your CustomRepositoryInterface, you must pass in a parameter of CustomInterface. Your module's di.xml has a preference to replace an interface of this type with an entity model. The entity model gets passed into the Resource Model and is saved.Set preference in
Namespace/Custom/etc/di.xml
: http://codepad.org/KmcoOUeVEntity model implementing Custom Interface (Data Container)
Namespace/Custom/Model/Custom.php
: http://codepad.org/xQiBU7p7 .Resource model
Namespace/Custom/Model/ResourceModel/Custom.php
: http://codepad.org/IOsxm9qW
A few things to note:
Disclaimer!!! I used "Namespace" in place of your custom vendor name, agency name, etc...whatever name you use to group your modules together...the actual use of "Namespace" is entirely not valid in Php...so know that I did this for convenience sake, and that I do not think this will work, nor do I suggest it in any way.
@Ryan Street taught me this...so I don't want to take all the credit
Clearly change the implementation of the Repository to suit your needs
You implement the interaction with your custom entity models/resource models/collections in the concrete Repository...
I know I didn't address all the methods that you listed in your question, but this is a great start and should bridge the gap between the docs and actual implementation.
complete files of using service contracts
Custom/Module/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Custom_Module',
__DIR__
);
../etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Custom_Module" setup_version="1.0.0" />
</config>
../Setup/InstallSchema.php
<?php
namespace Custom\Module\Setup;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;
class InstallSchema implements InstallSchemaInterface {
public function install( SchemaSetupInterface $setup, ModuleContextInterface $context ) {
$installer = $setup;
$installer->startSetup();
$table = $installer->getConnection()->newTable(
$installer->getTable( 'ad_shipping_quote' )
)->addColumn(
'entity_id',
Table::TYPE_SMALLINT,
null,
[ 'identity' => true, 'nullable' => false, 'primary' => true ],
'Post ID'
)->addColumn(
'product_id',
Table::TYPE_SMALLINT,
255,
[ ],
'Post ID'
)
->addColumn(
'customer_name',
Table::TYPE_TEXT,
255,
[ 'nullable' => false ],
'Post Title'
)
->addColumn(
'customer_email',
Table::TYPE_TEXT,
'2M',
[ ],
'Post Content'
) ->addColumn(
'customer_comments',
Table::TYPE_TEXT,
255,
[ 'nullable' => false ],
'Post Title'
)->addColumn(
'date_added',
Table::TYPE_TEXT,
255,
[ 'nullable' => false ],
'Post Title'
)->addColumn(
'date_updated',
Table::TYPE_TEXT,
255,
[ 'nullable' => false ],
'Post Title'
)
->setComment(
'Ad Shipping Quote Table'
);
$installer->getConnection()->createTable( $table );
$installer->endSetup();
}
}
../etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Custom\Module\Api\ModelRepositoryInterface"
type="Custom\Module\Model\ModelRepository" />
<preference for="Custom\Module\Api\Data\ModelInterface"
type="Custom\Module\Model\Model" />
<preference for="Custom\Module\Api\Data\ModelSearchResultsInterface"
type="Custom\Module\Model\ModelSearchResults" />
</config>
../etc/webapi.xml
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route method="GET" url="/V1/model/:id">
<service class="Custom\Module\Api\ModelRepositoryInterface" method="getById"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
<route method="GET" url="/V1/model">
<service class="Custom\Module\Api\ModelRepositoryInterface" method="getList"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
</routes>
../Api/ModelRepositoryInterface.php
<?php
namespace Custom\Module\Api;
use \Custom\Module\Api\Data\ModelInterface;
use \Magento\Framework\Api\SearchCriteriaInterface;
interface ModelRepositoryInterface
{
/**
* @api
* @param \Custom\Module\Api\Data\ModelInterface $model
* @return \Custom\Module\Api\Data\ModelInterface
*/
public function save(ModelInterface $model);
/**
* @api
* @param \Custom\Module\Api\Data\ModelInterface $model
* @return \Custom\Module\Api\Data\ModelInterface
*/
public function delete(ModelInterface $model);
/**
* @api
* @param \Custom\Module\Api\Data\ModelInterface $id
* @return void
*/
public function deleteById($id);
/**
* @api
* @param int $id
* @return \Custom\Module\Api\Data\ModelInterface
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getById($id);
/**
* @api
* @param \Magento\Framework\Api\SearchCriteriaInterface $criteria
* @return \Custom\Module\Api\Data\ModelSearchResultsInterface
*/
public function getList(SearchCriteriaInterface $criteria);
}
../Api/Data/ModelInterface.php
<?php
namespace Custom\Module\Api\Data;
interface ModelInterface
{
/**
* Return the Entity ID
*
* @return int
*/
public function getEntityId();
/**
* Set Entity ID
*
* @param int $id
* @return $this
*/
public function setEntityId($id);
/**
* Return the Product ID associated with Quote
*
* @return int
*/
public function getProductId();
/**
* Set the Product ID associated with Quote
*
* @param int $productId
* @return $this
*/
public function setProductId($productId);
/**
* Return the Customer Name
*
* @return string
*/
public function getCustomerName();
/**
* Set the Customer Name
*
* @param string $customerName
* @return $this
*/
public function setCustomerName($customerName);
/**
* Return the Customer Email
*
* @return string
*/
public function getCustomerEmail();
/**
* Set the Customer Email
*
* @param string $customerEmail
* @return $this
*/
public function setCustomerEmail($customerEmail);
/**
* Return the Customer Comments
*
* @return string
*/
public function getCustomerComments();
/**
* Set the Customer Comments
*
* @param string $customerComments
* @return $this
*/
public function setCustomerComments($customerComments);
/**
* Return the Date and Time of record added
*
* @return string
*/
public function getDateAdded();
/**
* Set the Date and Time of record added
*
* @param string $date
* @return $this
*/
public function setDateAdded($date);
/**
* Return the Date and Time of record updated
*
* @return string
*/
public function getDateUpdated();
/**
* Set the Date and Time of record updated
*
* @param string $date
* @return $this
*/
public function setDateUpdated($date);
}
..Api/Data/ModelSearchResultsInterface.php
<?php
namespace Custom\Module\Api\Data;
use Magento\Framework\Api\SearchResultsInterface;
interface ModelSearchResultsInterface extends SearchResultsInterface
{
/**
* @return \Custom\Module\Api\Data\ModelInterface[]
*/
public function getItems();
/**
* @param \Custom\Module\Api\Data\ModelInterface[] $items
* @return $this
*/
public function setItems(array $items);
}
../Model/Model.php
<?php
namespace Custom\Module\Model;
use Custom\Module\Api\Data\ModelInterface;
class Model extends \Magento\Framework\Model\AbstractModel implements
\Custom\Module\Api\Data\ModelInterface
{
protected function _construct()
{
$this->_init('Custom\Module\Model\ResourceModel\Model');
}
/**
* @inheritdoc
*/
public function getEntityId()
{
return $this->_getData('entity_id');
}
/**
* @inheritdoc
*/
public function setEntityId($id)
{
$this->setData('entity_id', $id);
}
/**
* @inheritdoc
*/
public function getProductId()
{
return $this->_getData('product_id');
}
/**
* @inheritdoc
*/
public function setProductId($productId)
{
$this->setData('product_id', $productId);
}
/**
* @inheritdoc
*/
public function getCustomerName()
{
return $this->_getData('customer_name');
}
/**
* @inheritdoc
*/
public function setCustomerName($customerName)
{
$this->setData('customer_name', $customerName);
}
/**
* @inheritdoc
*/
public function getCustomerEmail()
{
return $this->_getData('customer_email');
}
/**
* @inheritdoc
*/
public function setCustomerEmail($customerEmail)
{
$this->setData('customer_email', $customerEmail);
}
/**
* @inheritdoc
*/
public function getCustomerComments()
{
return $this->_getData('customer_comments');
}
/**
* @inheritdoc
*/
public function setCustomerComments($customerComments)
{
$this->setData('customer_comments', $customerComments);
}
/**
* @inheritdoc
*/
public function getDateAdded()
{
return $this->_getData('date_added');
}
/**
* @inheritdoc
*/
public function setDateAdded($date)
{
$this->setData('date_added', $date);
}
/**
* @inheritdoc
*/
public function getDateUpdated()
{
return $this->_getData('date_updated');
}
/**
* @inheritdoc
*/
public function setDateUpdated($date)
{
$this->setData('date_updated', $date);
}
}
../Model/ResourceModel/Model.php
<?php
namespace Custom\Module\Model\ResourceModel;
class Model extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
protected $_idFieldName = 'entity_id';
protected function _construct()
{
$this->_init('ad_shipping_quote','entity_id');
}
}
../Model/ResourceModel/Model/Collection.php
<?php
namespace Custom\Module\Model\ResourceModel\Model;
class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
{
protected $_idFieldName = 'entity_id';
protected $_eventPrefix = 'ad_shipping_quote_collection';
protected $_eventObject = 'quote_collection';
protected function _construct()
{
$this->_init('Custom\Module\Model\Model', 'Custom\Module\Model\ResourceModel\Model');
}
}
../Model/ModelRepository.php
<?php
namespace Custom\Module\Model;
use \Custom\Module\Api\Data\ModelInterface;
use \Custom\Module\Model\ResourceModel\Model as ObjectResourceModel;
use \Magento\Framework\Api\SearchCriteriaInterface;
use \Magento\Framework\Exception\CouldNotSaveException;
use \Magento\Framework\Exception\NoSuchEntityException;
use \Magento\Framework\Exception\CouldNotDeleteException;
class ModelRepository implements \Custom\Module\Api\ModelRepositoryInterface
{
protected $objectFactory;
protected $objectResourceModel;
protected $collectionFactory;
protected $searchResultsFactory;
public function __construct(
\Custom\Module\Model\ModelFactory $objectFactory,
ObjectResourceModel $objectResourceModel,
\Custom\Module\Model\ResourceModel\Model\CollectionFactory $collectionFactory,
\Magento\Framework\Api\SearchResultsInterfaceFactory $searchResultsFactory
) {
$this->objectFactory = $objectFactory;
$this->objectResourceModel = $objectResourceModel;
$this->collectionFactory = $collectionFactory;
$this->searchResultsFactory = $searchResultsFactory;
}
public function save(ModelInterface $object)
{
$name = $object->getCustomerName();
$hasSpouse = $object->getSpouse();
if ($hasSpouse == true) {
$name = "Mrs. " . $name;
} else {
$name = "Miss. " . $name;
}
$object->setCustomerName($name);
try {
$this->objectResourceModel->save($object);
} catch (\Exception $e) {
throw new CouldNotSaveException(__($e->getMessage()));
}
return $object;
}
/**
* @inheritdoc
*/
public function getById($id)
{
$object = $this->objectFactory->create();
$this->objectResourceModel->load($object, $id);
if (!$object->getId()) {
throw new NoSuchEntityException(__('Object with id "%1" does not exist.', $id));
}
return $object;
}
public function delete(ModelInterface $object)
{
try {
$this->objectResourceModel->delete($object);
} catch (\Exception $exception) {
throw new CouldNotDeleteException(__($exception->getMessage()));
}
return true;
}
public function deleteById($id)
{
return $this->delete($this->getById($id));
}
/**
* @inheritdoc
*/
public function getList(SearchCriteriaInterface $criteria)
{
$searchResults = $this->searchResultsFactory->create();
$searchResults->setSearchCriteria($criteria);
$collection = $this->collectionFactory->create();
foreach ($criteria->getFilterGroups() as $filterGroup) {
$fields = [];
$conditions = [];
foreach ($filterGroup->getFilters() as $filter) {
$condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq';
$fields[] = $filter->getField();
$conditions[] = [$condition => $filter->getValue()];
}
if ($fields) {
$collection->addFieldToFilter($fields, $conditions);
}
}
$searchResults->setTotalCount($collection->getSize());
$sortOrders = $criteria->getSortOrders();
if ($sortOrders) {
/** @var SortOrder $sortOrder */
foreach ($sortOrders as $sortOrder) {
$collection->addOrder(
$sortOrder->getField(),
($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC'
);
}
}
$collection->setCurPage($criteria->getCurrentPage());
$collection->setPageSize($criteria->getPageSize());
$objects = [];
foreach ($collection as $objectModel) {
$objects[] = $objectModel;
}
$searchResults->setItems($objects);
return $searchResults;
}
}
../Model/ModelSearchResults.php
namespace Custom\Module\Model;
use \Magento\Framework\Api\SearchResults;
use \Custom\Module\Api\Data\ModelSearchResultsInterface;
class ModelSearchResults extends SearchResults implements ModelSearchResultsInterface
{
}
../Controller/Index/Save.php
<?php
namespace Custom\Module\Controller\Index;
use \Magento\Framework\Controller\Result\RawFactory;
class Save extends \Magento\Framework\App\Action\Action
{
/**
* Index resultPageFactory
* @var PageFactory
*/
private $resultPageFactory;
/**
* @var
*/
private $modelFactory;
/**
* @var
*/
private $modelRepository;
/**
* Index constructor.
* @param \Magento\Framework\App\Action\Context $context
* @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
* @param \Custom\Module\Model\ModelFactory $modelFactory
* @param \Custom\Module\Model\ModelRepository $modelRepository
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $resultPageFactory,
\Custom\Module\Model\ModelFactory $modelFactory,
\Custom\Module\Model\ModelRepository $modelRepository
) {
$this->resultPageFactory = $resultPageFactory;
$this->modelFactory = $modelFactory;
$this->modelRepository = $modelRepository;
return parent::__construct($context);
}
/**
* @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface
*/
public function execute()
{
$data = [
"product_id" => 201,
"customer_name" => "Katrina",
"customer_email" => "[email protected]",
"spouse" => 1
];
$obj = $this->modelFactory->create();
$this->modelRepository->save($obj->addData($data)); // Service Contract
//$obj->addData($data)->save(); // Model / Resource Model
$this->resultFactory->create("raw");
}
}
../Controller/Index/Getlist.php
<?php
namespace Custom\Module\Controller\Index;
use \Magento\Framework\Controller\Result\RawFactory;
class Getlist extends \Magento\Framework\App\Action\Action
{
/**
* Index resultPageFactory
* @var PageFactory
*/
private $resultPageFactory;
/**
* @var
*/
private $modelFactory;
/**
* @var
*/
private $modelRepository;
/**
* @var
*/
private $searchCriteriaBuilder;
/**
* Index constructor.
* @param \Magento\Framework\App\Action\Context $context
* @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
* @param \Custom\Module\Model\ModelRepository $modelRepository
* @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $resultPageFactory,
\Custom\Module\Model\ModelRepository $modelRepository,
\Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder
) {
$this->resultPageFactory = $resultPageFactory;
$this->modelRepository = $modelRepository;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
return parent::__construct($context);
}
/**
* @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface
*/
public function execute()
{
$_filter = $this->searchCriteriaBuilder
->addFilter("customer_name", "%na%", "like")->create();
$list = $this->modelRepository->getList($_filter);
$results = $list->getItems();
foreach ($results as $result) {
echo $result->getCustomerName() . "<br>";
}
$this->resultFactory->create("raw");
}
}
../Controller/Index/Getbyid.php
<?php
namespace Custom\Module\Controller\Index;
use \Magento\Framework\Controller\Result\RawFactory;
class Getbyid extends \Magento\Framework\App\Action\Action
{
/**
* Index resultPageFactory
* @var PageFactory
*/
private $resultPageFactory;
/**
* @var
*/
private $modelRepository;
/**
* Index constructor.
* @param \Magento\Framework\App\Action\Context $context
* @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
* @param \Custom\Module\Model\ModelRepository $modelRepository
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $resultPageFactory,
\Custom\Module\Model\ModelRepository $modelRepository
) {
$this->resultPageFactory = $resultPageFactory;
$this->modelRepository = $modelRepository;
return parent::__construct($context);
}
public function execute()
{
$search = $this->modelRepository->getById(1);
print_r($search->getData());
$this->resultFactory->create("raw");
}
}
../Controller/Index/Deletebyid.php
<?php
namespace Custom\Module\Controller\Index;
use \Magento\Framework\Controller\Result\RawFactory;
class Deletbyid extends \Magento\Framework\App\Action\Action
{
/**
* Index resultPageFactory
* @var PageFactory
*/
private $resultPageFactory;
/**
* @var
*/
private $modelRepository;
/**
* Index constructor.
* @param \Magento\Framework\App\Action\Context $context
* @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
* @param \Custom\Module\Model\ModelRepository $modelRepository
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $resultPageFactory,
\Custom\Module\Model\ModelRepository $modelRepository
) {
$this->resultPageFactory = $resultPageFactory;
$this->modelRepository = $modelRepository;
return parent::__construct($context);
}
public function execute()
{
$this->modelRepository->deleteById(1);
$this->resultFactory->create("raw");
}
}
../Controller/Index/Del.php
<?php
namespace Custom\Module\Controller\Index;
use \Magento\Framework\Controller\Result\RawFactory;
class Del extends \Magento\Framework\App\Action\Action
{
/**
* Index resultPageFactory
* @var PageFactory
*/
private $resultPageFactory;
/**
* @var
*/
private $modelRepository;
/**
* Index constructor.
* @param \Magento\Framework\App\Action\Context $context
* @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
* @param \Custom\Module\Model\ModelFactory $modelFactory
* @param \Custom\Module\Model\ModelRepository $modelRepository
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $resultPageFactory,
\Custom\Module\Model\ModelFactory $modelFactory,
\Custom\Module\Model\ModelRepository $modelRepository
) {
$this->resultPageFactory = $resultPageFactory;
$this->modelFactory = $modelFactory;
$this->modelRepository = $modelRepository;
return parent::__construct($context);
}
public function execute()
{
$obj = $this->modelFactory->create()->load(2);
$this->modelRepository->delete($obj);
$this->resultFactory->create("raw");
}
}