Shopware 6 - Custom CMS elements: How to get the data from two repositories into a custom CMS block? - shopware

We are working on a custom shopware 6 plugin. This plugin contains a custom CMS block with two CMS elements. The CMS elements need data from two different custom repositories which are not associated to each other.
We tried to set up two DataResolver (one for each CMS element) but they are overwriting each other. So we tried to put everything in one DataResolver. We checked how some other plugins do this but it is not very clear to us.
Can someone help us what we need to put into the CMSElementReslover.php file especially into these two methods:
public function collect(CmsSlotEntity $slot, ResolverContext $resolverContext): ?CriteriaCollection
{
// Some other plugins return null here(?)
}
public function enrich(CmsSlotEntity $slot, ResolverContext $resolverContext, ElementDataCollection $result): void
{
// If we return null at the collect method, our $result variable is empty.
// We need the two custom repositories here...
}
We setup two additional classes for each element which extends our CmsElementResolver, e.g. like this:
class ExampleOneCmsElementResolver extends CmsElementResolver
{
public function getType(): string
{
}
public function getStruct(): Struct
{
}
The documentation is not very detailed: https://developer.shopware.com/docs/guides/plugins/plugins/content/cms/add-data-to-cms-elements
Thanks for any hint! 🙏🏼
Best!
#shopware #shopware6
We tried to add both repositories to the collection, e.g. like this:
public function collect(CmsSlotEntity $slot, ResolverContext $resolverContext): ?CriteriaCollection
{
$config = $slot->getFieldConfig();
$criteria = new Criteria();
$criteriaCollection = new CriteriaCollection();
$criteriaCollection->add(
Entity1Definition::ENTITY_NAME,
Entity1Definition::class,
$criteria
);
$criteria = new Criteria();
$criteriaCollection->add(
Entity2Definition::ENTITY_NAME,
Entity2Definition::class,
$criteria
);
return $criteriaCollection->all() ? $criteriaCollection : null;
}

You are on the right track with adding your criteria to the collection.
Let's say you want to fetch a media and a product entity.
In the collect method you simply provide a unique key with both your criteria.
In the enrich method you retrieve the result of the respective search with the same key you used for the criteria.
Then it's simply a matter of setting your entities you retrieve from the search results to the slot by using an instance of Struct, e.g. ArrayStruct.
public function collect(CmsSlotEntity $slot, ResolverContext $resolverContext): ?CriteriaCollection
{
$mediaIdConfig = $slot->getFieldConfig()->get('mediaId');
$productIdConfig = $slot->getFieldConfig()->get('productId');
if ($mediaIdConfig === null || $productIdConfig === null) {
return null;
}
$criteriaCollection = new CriteriaCollection();
$criteria = new Criteria([$mediaIdConfig->getStringValue()]);
$criteriaCollection->add('media_' . $slot->getUniqueIdentifier(), MediaDefinition::class, $criteria);
$criteria = new Criteria([$productIdConfig->getStringValue()]);
$criteriaCollection->add('product_' . $slot->getUniqueIdentifier(), ProductDefinition::class, $criteria);
return $criteriaCollection;
}
public function enrich(CmsSlotEntity $slot, ResolverContext $resolverContext, ElementDataCollection $result): void
{
$mediaSearchResult = $result->get('media_' . $slot->getUniqueIdentifier());
$productSearchResult = $result->get('product_' . $slot->getUniqueIdentifier());
if ($mediaSearchResult === null || $productSearchResult === null) {
return;
}
$slot->setData(new ArrayStruct([
'media' => $mediaSearchResult->first(),
'product' => $productSearchResult->first(),
]));
}

Related

Is there a way to iterate through the fields in a row of a PXResultSet?

Is it possible to use a foreach loop in a BLC to iterate through the fields of a PXResultSet to get the FieldNames?
Is this doable? I can't seem to find a good way.
Thanks...
The PXResultset records are selected from a view. You can get the field names from the View.
Here's a full example:
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
public override void Initialize()
{
// Get field list from data view
var dataView = new PXSelect<SOOrder>(Base);
string fieldNames = string.Join(",", GetFieldNames(dataView.View, Base.Caches));
// You don't need result set to get field names
PXResultset<SOOrder> resultSet = dataView.Select();
throw new PXException(fieldNames);
}
public string[] GetFieldNames(PXView view, PXCacheCollection caches)
{
var list = new List<string>();
var set = new HashSet<string>();
foreach (Type t in view.GetItemTypes())
{
if (list.Count == 0)
{
list.AddRange(caches[t].Fields);
set.AddRange(list);
}
else
{
foreach (string field in caches[t].Fields)
{
string s = String.Format("{0}__{1}", t.Name, field);
if (set.Add(s))
{
list.Add(s);
}
}
}
}
return list.ToArray();
}
}
When run, this example will show the fields names used in the data view in Sales Order screen SO301000 as an exception.
Field names are contained in Cache object. If you really need to get field names from PXResultset you need to iterate the cache types in the result set.
Example for first DacType (0) of result set:
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
public override void Initialize()
{
var dataView = new PXSelect<SOOrder>(Base);
PXResultset<SOOrder> resultSet = dataView.Select();
foreach (PXResult result in resultSet)
{
Type dacType = result.GetItemType(0);
foreach (var field in Base.Caches[dacType].Fields)
PXTrace.WriteInformation(field);
}
}
}

Shopware 6: Extend existing core DAL entities with a reference

I'm trying to create a connection between 2 existing entities PropertyGroup and CustomFieldSet. Use-case is irrelevant.
So I created an EntityExtension:
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new ManyToOneAssociationField('customFieldSet', 'custom_field_set', CustomFieldSetDefinition::class))
);
}
public function getDefinitionClass(): string
{
return PropertyGroupDefinition::class;
}
And override the administration component to also include this association when loading the entity:
Component.override('sw-property-detail', {
methods: {
loadEntityData() {
this.isLoading = true;
const criteria = this.defaultCriteria;
criteria.addAssociation('customFieldSet', new Criteria(1, 500));
this.propertyRepository.get(this.groupId, Shopware.Context.api, criteria)
.then((currentGroup) => {
this.propertyGroup = currentGroup;
this.isLoading = false;
}).catch(() => {
this.isLoading = false;
});
}
}
});
(I tried to override defaultCriteria but that didn't work because of this.$super being unable to access computed properties).
But it keeps saying FRAMEWORK__ASSOCIATION_NOT_FOUND. I debugged the EntityDefinition and it seems that this extension is not even loaded.
I checked if my EntityExtension is loaded in the Symfony container and it is, but it seems that it doesn't reach the entity definition.
The EntityExtension seems to be missing the addition of a FkField inside the function extendFields:
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new FkField('custom_field_set', 'customFieldSetId', CustomFieldSetDefinition::class)),
);
$collection->add(
(new ManyToOneAssociationField('customFieldSet', 'custom_field_set', CustomFieldSetDefinition::class))
);
}
A new use statement has to be added for the FkField:
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;

ASP.NET Core 1 Web API Model Binding Array

How do you model bind an array from the URI with GET in ASP.NET Core 1 Web API (implicitly or explicitly)?
In ASP.NET Web API pre Core 1, this worked:
[HttpGet]
public void Method([FromUri] IEnumerable<int> ints) { ... }
How do you do this in ASP.NET Web API Core 1 (aka ASP.NET 5 aka ASP.NET vNext)? The docs have nothing.
The FromUriAttribute class combines the FromRouteAttribute and FromQueryAttribute classes. Depending the configuration of your routes / the request being sent, you should be able to replace your attribute with one of those.
However, there is a shim available which will give you the FromUriAttribute class. Install the "Microsoft.AspNet.Mvc.WebApiCompatShim" NuGet package through the package explorer, or add it directly to your project.json file:
"dependencies": {
"Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-rc1-final"
}
While it is a little old, I've found that this article does a pretty good job of explaining some of the changes.
Binding
If you're looking to bind comma separated values for the array ("/api/values?ints=1,2,3"), you will need a custom binder just as before. This is an adapted version of Mrchief's solution for use in ASP.NET Core.
public class CommaDelimitedArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.IsEnumerableType)
{
var key = bindingContext.ModelName;
var value = bindingContext.ValueProvider.GetValue(key).ToString();
if (!string.IsNullOrWhiteSpace(value))
{
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
var converter = TypeDescriptor.GetConverter(elementType);
var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => converter.ConvertFromString(x.Trim()))
.ToArray();
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Result = ModelBindingResult.Success(typedValues);
}
else
{
// change this line to null if you prefer nulls to empty arrays
bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(bindingContext.ModelType.GetElementType(), 0));
}
return TaskCache.CompletedTask;
}
return TaskCache.CompletedTask;
}
}
You can either specify the model binder to be used for all collections in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc().AddMvcOptions(opts =>
{
opts.ModelBinders.Insert(0, new CommaDelimitedArrayModelBinder());
});
}
Or specify it once in your API call:
[HttpGet]
public void Method([ModelBinder(BinderType = typeof(CommaDelimitedArrayModelBinder))] IEnumerable<int> ints)
ASP.NET Core 1.1 Answer
#WillRay's answer is a little outdated. I have written an 'IModelBinder' and 'IModelBinderProvider'. The first can be used with the [ModelBinder(BinderType = typeof(DelimitedArrayModelBinder))] attribute, while the second can be used to apply the model binder globally as I've show below.
.AddMvc(options =>
{
// Add to global model binders so you don't need to use the [ModelBinder] attribute.
var arrayModelBinderProvider = options.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
options.ModelBinderProviders.Insert(
options.ModelBinderProviders.IndexOf(arrayModelBinderProvider),
new DelimitedArrayModelBinderProvider());
})
public class DelimitedArrayModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsEnumerableType && !context.Metadata.ElementMetadata.IsComplexType)
{
return new DelimitedArrayModelBinder();
}
return null;
}
}
public class DelimitedArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
var values = valueProviderResult
.ToString()
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
if (values.Length == 0)
{
bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(elementType, 0));
}
else
{
var converter = TypeDescriptor.GetConverter(elementType);
var typedArray = Array.CreateInstance(elementType, values.Length);
try
{
for (int i = 0; i < values.Length; ++i)
{
var value = values[i];
var convertedValue = converter.ConvertFromString(value);
typedArray.SetValue(convertedValue, i);
}
}
catch (Exception exception)
{
bindingContext.ModelState.TryAddModelError(
modelName,
exception,
bindingContext.ModelMetadata);
}
bindingContext.Result = ModelBindingResult.Success(typedArray);
}
return Task.CompletedTask;
}
}
There are some changes in the .NET Core 3.
Microsoft has split out the functionality from the AddMvc method (source).
As AddMvc also includes support for View Controllers, Razor Views and etc. If you don't need to use them in your project (like in an API), you might consider using services.AddControllers() which is for Web API controllers.
So, updated code will look like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddMvcOptions(opt =>
{
var mbp = opt.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
opt.ModelBinderProviders.Insert(opt.ModelBinderProviders.IndexOf(mbp), new DelimitedArrayModelBinderProvider());
});
}

Orchard Custom Workflow Activity

I have built a custom module in Orchard that creates a new part, type and a custom activity but I'm struggling with the last part of what I need to do which is to create a copy of all the content items associated with a specific parent item.
For instance, when someone creates a "Trade Show" (new type from my module), various subpages can be created off of it (directions, vendor maps, etc.) since the client runs a single show at a time. What I need to do is, when they create a new Trade Show, I want to get the most recent prior show (which I'm doing via _contentManager.HqlQuery().ForType("TradeShow").ForVersion(VersionOptions.Latest).ForVersion(VersionOptions.Published).List().Last() (positive that's not the most efficient way, but it works and the record count would be ~10 after five years), then find all of those child pages that correlate to that old show and copy them into new Content Items. They have to be a copy because on occasion they may have to refer back to parts with the old shows, or it could change, etc. All the usual reasons.
How do I go about finding all of the content items that reference that prior show in an Activity? Here is my full class for the Activity:
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Autoroute.Services;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Projections.Models;
using Orchard.Projections.Services;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
using Orchard.Workflows.Activities;
namespace Orchard.Web.Modules.TradeShows.Activities
{
public class TradeShowPublishedActivity : Task
{
private readonly IContentManager _contentManager;
private readonly IAutorouteService _autorouteService;
private readonly IProjectionManager _projectionManager;
public TradeShowPublishedActivity(IContentManager contentManager, IAutorouteService autorouteService, IProjectionManager projectionManager)
{
_contentManager = contentManager;
_autorouteService = autorouteService;
_projectionManager = projectionManager;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override LocalizedString Category
{
get { return T("Flow"); }
}
public override LocalizedString Description
{
get { return T("Handles the automatic creation of content pages for the new show."); }
}
public override string Name
{
get { return "TradeShowPublished"; }
}
public override string Form
{
get { return null; }
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext)
{
yield return T("Done");
}
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext)
{
var priorShow = _contentManager.HqlQuery().ForType("TradeShow").ForVersion(VersionOptions.Latest).ForVersion(VersionOptions.Published).List().Last();
var tradeShowPart = priorShow.Parts.Where(p => p.PartDefinition.Name == "TradeShowContentPart").Single();
//new show alias
//workflowContext.Content.ContentItem.As<Orchard.Autoroute.Models.AutoroutePart>().DisplayAlias
yield return T("Done");
}
}
}
My Migrations.cs file sets up the part that is used for child pages to reference the parent show like this:
ContentDefinitionManager.AlterPartDefinition("AssociatedTradeShowPart", builder => builder.WithField("Trade Show", cfg => cfg.OfType("ContentPickerField")
.WithDisplayName("Trade Show")
.WithSetting("ContentPickerFieldSettings.Attachable", "true")
.WithSetting("ContentPickerFieldSettings.Description", "Select the trade show this item is for.")
.WithSetting("ContentPickerFieldSettings.Required", "true")
.WithSetting("ContentPickerFieldSettings.DisplayedContentTypes", "TradeShow")
.WithSetting("ContentPickerFieldSettings.Multiple", "false")
.WithSetting("ContentPickerFieldSettings.ShowContentTab", "true")));
Then, my child pages (only one for now, but plenty more coming) are created like this:
ContentDefinitionManager.AlterTypeDefinition("ShowDirections", cfg => cfg.DisplayedAs("Show Directions")
.WithPart("AutoroutePart", builder => builder.WithSetting("AutorouteSettings.AllowCustomPattern", "true")
.WithSetting("AutorouteSettings.AutomaticAdjustmentOnEdit", "false")
.WithSetting("AutorouteSettings.PatternDefinitions", "[{Name:'Title', Pattern: '{Content.Slug}', Description: 'international-trade-show'}]")
.WithSetting("AutorouteSettings.DefaultPatternIndex", "0"))
.WithPart("CommonPart", builder => builder.WithSetting("DateEditorSettings.ShowDateEditor", "false"))
.WithPart("PublishLaterPart")
.WithPart("TitlePart")
.WithPart("AssociatedTradeShowPart") /* allows linking to parent show */
.WithPart("ContainablePart", builder => builder.WithSetting("ContainablePartSettings.ShowContainerPicker", "true"))
.WithPart("BodyPart"));
So you have the Trade Show content item, the next step will be to find all parts with a ContentPickerField, then filter that list down to those where the field contains your show's ID.
var items = _contentManager.Query().List().ToList() // Select all content items
.Select(p => (p.Parts
// Select all parts on content items
.Where(f => f.Fields.Where(d =>
d.FieldDefinition.Name == typeof(ContentPickerField).Name &&
// See if any of the fields are ContentPickerFields
(d as ContentPickerField).Ids.ToList().Contains(priorShow.Id)).Any())));
// That field contains the Id of the show
This could get expensive depending on how many content items are in your database.

How to insert multiple objects in to Azure Mobile Services table controller [.Net backend]

I have an Azure Mobile service coded in .net Web API. I have a TableController. I want that table controller to be able to insert multiple persons, not just one person with from the client with InsertAsync(myPerson). I have the following code in the TableController:
[RequiresAuthorization(AuthorizationLevel.Admin)]
public async Task<bool> InsertPersons(List<Person> values)
{
try
{
foreach (var item in values)
{
var current = await InsertAsync(item);
}
return true;
}
catch (System.Exception)
{
return false;
}
}
The problem is in the client. Because it is strongly typed it only allows me to insert one item at a time. How must I call the server from the client? Do I have to write a Custom Api Controller and call it with mobileService.InvokeApiAsync? If so, how can I get access to my database from a Custom API Controller that doesn't inherit from TableController?
Thank you so much!
The helper methods in the TableController<T> base class assume that the insert operations apply to a single object - and the InsertAsync method in the client also assumes the same. So even though you can define in a table controller a method that takes an array (or list) of Person, you won't be able to call it via the client SDK (at least not without some heavy-lifting using a handler, for example).
You can, however, create a custom API which takes such a list. And to insert the multiple items from the API, you can access the context directly, without needing to go through the helper methods from the table:
public class PersonController : ApiController
{
test20140807Context context;
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
this.context = new test20140807Context();
}
[HttpPost]
public async Task<bool> InsertPersons(List<Person> values)
{
foreach (var value in values)
{
if (string.IsNullOrEmpty(value.Id))
{
value.Id = Guid.NewGuid().ToString();
}
}
try
{
this.context.People.AddRange(values);
await this.context.SaveChangesAsync();
return true;
}
catch (System.Exception ex)
{
Trace.WriteLine("Error: " + ex);
return false;
}
}
}
And on the client:
private async void btnTest_Click(object sender, RoutedEventArgs e)
{
var items = new Person[]
{
new Person { Name = "John Doe", Age = 33 },
new Person { Name = "Jane Roe", Age = 32 }
};
try
{
var response = await App.MobileService.InvokeApiAsync<Person[], bool>("person", items);
Debug.WriteLine("response: " + response);
}
catch (Exception ex)
{
var str = ex.ToString();
Debug.WriteLine(str);
}
}
From Carlos Figueira's post on inserting multiple items at once in azure mobile services, it looks like what you need to do is create another table called AllPersons. In your client, the AllPersons object would have a Persons array member. In your server side script for the AllPersons insert, you iterate through the AllPersons.Persons and insert into the table one by one.

Resources