Extend Woocommerce Rest API class & change schema for /orders endpoint - woocommerce

I am calling the woocommerce REST api to create an order : /wp-json/wc/v3/orders with quantity as a float value, but the REST api expects an integer quantity value, hence the response errors out.
Response from the POST call below
I tried extending the Base API class in functions.php of child-theme to override the quantity type to float, but thats not working. What ma I missing here?
class CUSTOM_WC_REST_Orders_Controller extends WC_REST_Orders_Controller
{
public function get_item_schema()
{
$schema = parent::get_item_schema();
$schema['properties']['line_items']['items']['properties']['quantity']['type'] = 'float';
return $schema;
}
}
new CUSTOM_WC_REST_Orders_Controller();
While I try the same approach to test with a POST on products api, it seems to work. For my testing purpose i changed the regular_price property field to string and it works. Code below for reference.
class CUSTOM_WC_REST_Product_Controller extends WC_REST_Products_Controller
{
public function get_item_schema()
{
$schema = parent::get_item_schema();
$schema["properties"]["regular_price"]["type"] = "string";
return $schema;
}
}
new CUSTOM_WC_REST_Product_Controller();
Why does extending REST Base class for products work & not orders?

Related

Removing some schemas/models from API-Platforms Swagger/OpenAPI documentation output

API-Platform will generate Swagger/OpenAPI route documentation and then below documentation for the Schemas (AKA Models) (the docs show them as "Models" but current versions such as 2.7 show them as "Schemas").
Where is the content generated to show these schemas/models? How can some be removed? The functionality to display them is part of Swagger-UI, but API-Platform must be responsible for providing the JSON configuration and thus which to change API-Platform and not Swagger-UI. Note that this post shows how to add a schema but not how to remove one. Is there any documentation on the subject other than this which doesn't go into detail?
As seen by the output below, I am exposing AbstractOrganization, however, this class is extended by a couple other classes and is not meant to be exposed, but only schemas for the concrete classes should be exposed. Note that my AbstractOrganization entity class is not tagged with #ApiResource and is not shown in the Swagger/OpenAPI routing documentation but only the schema/model documentation.
Thank you
I am pretty certain there are better ways to implement this, however, the following will work and might be helpful for others.
<?php
declare(strict_types=1);
namespace App\OpenApi;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\OpenApi;
use ApiPlatform\Core\OpenApi\Model\Paths;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class OpenApiRouteHider implements OpenApiFactoryInterface {
public function __construct(private OpenApiFactoryInterface $decorated, private TokenStorageInterface $tokenStorage)
{
}
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
$removedPaths = $this->getRemovedPaths();
$paths = new Paths;
$pathArray = $openApi->getPaths()->getPaths();
foreach($openApi->getPaths()->getPaths() as $path=>$pathItem) {
if(!isset($removedPaths[$path])) {
// No restrictions
$paths->addPath($path, $pathItem);
}
elseif($removedPaths[$path]!=='*') {
// Remove one or more operation
foreach($removedPaths[$path] as $operation) {
$method = 'with'.ucFirst($operation);
$pathItem = $pathItem->$method(null);
}
$paths->addPath($path, $pathItem);
}
// else don't add this route to the documentation
}
$openApiTest = $openApi->withPaths($paths);
return $openApi->withPaths($paths);
}
private function getRemovedPaths():array
{
// Use $user to determine which ones to remove.
$user = $this->tokenStorage->getToken()->getUser();
return [
'/guids'=>'*', // Remove all operations
'/guids/{guid}'=>'*', // Remove all operations
'/tenants'=>['post', 'get'], // Remove only post and get operations
'/tenants/{uuid}'=>['delete'], // Remove only delete operation
'/chart_themes'=>'*',
'/chart_themes/{id}'=>['put', 'delete', 'patch'],
];
}
}

How to denormalize an array recursively in Symfony 5?

I am currently trying to denormalize an array, which came out of an API as a JSON response and was JSON decoded.
The problem is, that I want it to be denormalized into a class and one of the properties is another class.
It feels like it should be possible to get such an easy job done with the Symfony denormalizer, but I always get the following exception:
Failed to denormalize attribute "inner_property" value for class "App\Model\Api\Outer": Expected argument of type "App\Model\Api\Inner", "array" given at property path "inner_property".
My denormalizing code looks like that:
$this->denormalizer->denormalize($jsonOuter, Outer::class);
The denormalizer is injected in the constructor:
public function __construct(DenormalizerInterface $denormalizer) {
The array I try to denormalize:
array (
'inner_property' =>
array (
'property' => '12345',
),
)
Finally the both classes I try to denormalize to:
class Outer
{
/** #var InnerProperty */
private $innerProperty;
public function getInnerProperty(): InnerProperty
{
return $this->innerProperty;
}
public function setInnerProperty(InnerProperty $innerProperty): void
{
$this->innerProperty = $innerProperty;
}
}
class InnerProperty
{
private $property;
public function getProperty(): string
{
return $this->property;
}
public function setProperty(string $property): void
{
$this->property = $property;
}
}
After hours of searching I finally found the reason. The problem was the combination of the "inner_property" snake case and $innerProperty or getInnerProperty camel case. In Symfony 5 the camel case to snake case converter is not enabled by default.
So I had to do this by adding this config in the config/packages/framework.yaml:
framework:
serializer:
name_converter: 'serializer.name_converter.camel_case_to_snake_case'
Here is the reference to the Symfony documentation: https://symfony.com/doc/current/serializer.html#enabling-a-name-converter
Alternatively I could have also add a SerializedName annotation to the property in the Outer class:
https://symfony.com/doc/current/components/serializer.html#configure-name-conversion-using-metadata
PS: My question was not asked properly, because I didn't changed the property and class names properly. So I fixed that in the question for future visitors.

Audit.NET CustomFields keeping value between requests

I tried to use these two ways to write a customfield and it is recording correctly, but it is keeping customfields between requests
public class LoggerAudit : ILoggerAudit
{
public void AddOnSavingAction(string key, object value)
{
Configuration.AddOnSavingAction(scope =>
{
scope.SetCustomField(key, value);
//scope.Event.CustomFields.Remove(key);
//scope.Event.CustomFields.Add(key, value);
});
}
}
For example:
In the first request my webapi recorded the customfield 'field-A', but in the second request my webapi, there was no need to write this customfield, but it was kept in scope and consequently in my json
I tried this setting, but it didn't work
.WithAction(action =>
{
action.OnEventSaved(scope => scope.Event.CustomFields = new Dictionary<string, object>());
});
The custom actions attached with AddOnSavingAction / OnEventSaved are globally attached and will execute for each and all the events before saving or after saving occurs (respectively), so you should attach each action just once.
But your use case looks like you don't have a way to derive the custom field value from the audit scope, so a custom action will not be useful.
Also I guess you are using Audit.WebApi extension. If that's the case, you won't need a custom action to add a custom field, since you can access the AuditScope with the provided extension methods directly on your controllers or in any place where you can get the current HttpContext, for example:
using Audit.WebApi;
[AuditApi]
public class UsersController : Controller
{
public IHttpActionResult Get(string id)
{
//...
var auditScope = this.GetCurrentAuditScope();
auditScope.SetCustomField("MyField", Guid.NewGuid());
//...
}
}
or just
private void SetCustomField(HttpContext context, string key, object value)
{
var auditScope = context.GetCurrentAuditScope();
auditScope.SetCustomField(key, value);
}

PHP/Symfony - Parsing object properties from Request

We're building a REST API in Symfony and in many Controllers we're repeating the same code for parsing and settings properties of objects/entities such as this:
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
I'm aware that Symfony forms provide this functionality, however, we've decided in the company that we want to move away from Symfony forms and want to use something simplier and more customisable instead.
Could anybody please provide any ideas or examples of libraries that might achieve property parsing and settings to an object/entity? Thank you!
It seems like a good use case for ParamConverter. Basically it allows you, by using #ParamConverter annotation to convert params which are coming into your controller into anything you want, so you might just create ParamConverter with code which is repeated in many controllers and have it in one place. Then, when using ParamConverter your controller will receive your entity/object as a parameter.
class ExampleParamConverter implements ParamConverterInterface
{
public function apply(Request $request, ParamConverter $configuration)
{
//put any code you want here
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
//now you are setting object which will be injected into controller action
$request->attributes->set($configuration->getName(), $solution);
return true;
}
public function supports(ParamConverter $configuration)
{
return true;
}
}
And in controller:
/**
* #ParamConverter("exampleParamConverter", converter="your_converter")
*/
public function action(Entity $entity)
{
//you have your object available
}

Spring Security: separating controller by user role

With #RequestMapping, request can be associated with different controller functions through header or request parameters. Is there a way to achieve this base on the user user role? The aim is avoid if statement in the controller.
As far as I am aware, there is not anything that comes out of the box, but if you wanted to you could probably create a custom mapping annotation to do this routing for you.
I have not actually tried any of this code, but something like:
Your new annoation, used like #UserRoleMapping("ROLE_ADMIN")
#Target( ElementType.TYPE )
#Retention(RetentionPolicy.RUNTIME)
public #interface UserRoleMapping {
String[] value();
}
Next, you can just extend the standard Spring RequestMappingHandlerMapping class (this is the class that handles the standard mapping of #RequestMapping annotations). You just need to tell the mapping handler to also take into account a custom condition:
public class UserRoleRequestCondition extends RequestMappingHandlerMapping {
#Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
UserRoleMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, UserRoleMapping.class);
return (typeAnnotation != null) ? new UserRoleRequestCondition( typeAnnotation.value() ) : null;
}
}
The above code just checks the controller for your new annotation created above and if it is found it returns a new condition class, constructed with the value you have set in the annotation (e.g. "ROLE_ADMIN"). This MappingHandler will need to be set in your Spring config (whereever you are currently setting the RequestMappingHandlerMapping, just replace it with one of these).
Next we need to create the custom condition - this is the guy that is going to be invoked on request to determine if a request matches the controller:
public class UserRoleRequestCondition implements RequestCondition<UserRoleRequestCondition> {
private final Set<String> roles;
public UserRoleRequestCondition( String... roles ) {
this( Arrays.asList(roles) );
}
public UserRoleRequestCondition( Collection<String> roles ) {
this.roles = Collections.unmodifiableSet(new HashSet<String>(roles));
}
#Override public UserRoleRequestCondition combine(UserRoleRequestCondition other) {
Set<String> allRoles = new LinkedHashSet<String>(this.roles);
allRoles.addAll(other.roles);
return new UserRoleRequestCondition(allRoles);
}
#Override public UserRoleRequestCondition getMatchingCondition( HttpServletRequest request ) {
UserRoleRequestCondition condition = null;
for (String r : roles){
if ( request.isUserInRole( r ) ){
condition = this;
}
}
return condition;
}
#Override public int compareTo(UserRoleRequestCondition other, HttpServletRequest request) {
return (other.roles - this.roles).size();
}
}
In the above, the method getMatchingCondition is where we match the request. (apologies if I have missed some semi-colons or return keywords etc - this is based on groovy, but hopefully if you are in java you can work out where those bits go!)
Props to Marek for his more detailed answer on the more fully-formed solution to custom routing based on the subdomain that I used when I had to implement something similar! How to implement #RequestMapping custom properties - That gives more details about what is going on, and how to have method level annotations (this example skips that and only defines class level annotations)
I have also written up some notes on this here: http://automateddeveloper.blogspot.co.uk/2014/12/spring-mvc-custom-routing-conditions.html
Implement AuthenticationSuccessHandler onAuthenticationSuccess redirect to specific controller based on the User Role.

Resources