Magento 2 - Set product attribute to use default values - magento

I want to check Use as Default for all product for a particular store view
I use this code
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$collection = $this->_productCollectionFactory->create();
foreach($collection as $data){
$product=$this->_product->setStoreId(1)->load($data->getEntityId());
$product->setData('name',false);
$product->save();
}
But this is removing product from categories.
Can you please let me know how I can check Use as default checkbox programmatically.

Magento 2.1.3 EE
The following solution creates a CLI command to directly manipulate the database and delete store specific product attribute information. It was written for Magento Enterprise edition, so if you're using Community Edition you'll have to modify this code to utilize entity_id instead of row_id.
Please be careful with this. The solution laid forth here bypasses model classes and performs direct delete queries on the default database connection's catalog_product_entity_datetime, catalog_product_entity_decimal, catalog_product_entity_int, catalog_product_entity_text, and catalog_product_entity_varchar tables. Back up your database first.
Step 1: Create the module
app/code/StackOverflow/Question40177336/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'StackOverflow_Question40177336',
__DIR__
);
app/code/StackOverflow/Question40177336/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="StackOverflow_Question40177336" setup_version="0.0.1"/>
</config>
app/code/StackOverflow/Question40177336/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">
<type name="Magento\Framework\Console\CommandListInterface">
<arguments>
<argument name="commands" xsi:type="array">
<item name="stackoverflow_question40177336" xsi:type="object">StackOverflow\Question40177336\Console\Command\Product\UseDefaultValue</item>
</argument>
</arguments>
</type>
</config>
app/code/StackOverflow/Question40177336/Console/Command/Product/UseDefaultValue.php
<?php
namespace StackOverflow\Question40177336\Console\Command\Product;
use Magento\Catalog\Model\Product;
use Magento\Eav\Setup\EavSetup;
use Magento\Framework\App\ResourceConnection;
use Magento\Store\Model\StoreManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class UseDefaultValue extends Command
{
/**
* flag indicating if the command has been initialized yet
*
* #var bool
*/
protected $initialized = false;
/**
* The attribute_id to use for the current command.
*
* #var int
*/
protected $attributeId;
/**
* The row_id values(s) to use for the command (if any).
*
* #var array|bool
*/
protected $rowIds;
/**
* The store_id to use for the current command.
*
* #var int
*/
protected $storeId;
/**
* The table name to use for the current command.
*
* #var string
*/
protected $tableName;
/**
* #var \Magento\Framework\DB\Adapter\AdapterInterface
*/
protected $connection;
/**
* #var EavSetup
*/
protected $eavSetup;
/**
* #var StoreManagerInterface
*/
protected $storeManager;
public function __construct(
EavSetup $eavSetup,
ResourceConnection $resourceConnection,
StoreManagerInterface $storeManager
) {
$this->connection = $resourceConnection->getConnection();
$this->eavSetup = $eavSetup;
$this->storeManager = $storeManager;
parent::__construct();
}
/**
* Configures the current command.
*/
protected function configure()
{
$this
->setName('catalog:product:attributes:use-default-value')
->setDescription('Removes store specific data from a product(s) of given attribute code.')
->addArgument(
'attribute_code',
InputArgument::REQUIRED,
'Attribute Code'
)
->addArgument(
'store',
InputArgument::REQUIRED,
'Store code or store_id (cannot be \'admin\' or \'0\')'
)
->addArgument(
'sku',
InputArgument::OPTIONAL,
'Sku (omit to apply to all products)'
)
;
}
/**
* Executes the current command.
*
* #param InputInterface $input An InputInterface instance
* #param OutputInterface $output An OutputInterface instance
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->init($input);
$conn = $this->connection;
$bind = [
$conn->quoteInto('store_id = ?', $this->getStoreId()),
$conn->quoteInto('attribute_id = ?', $this->getAttributeId())
];
if ($this->getRowIds()) {
$bind[] = $conn->quoteInto('row_id IN (?)', $this->getRowIds());
}
$rows = $conn->delete($this->getTableName(), $bind);
$output->writeln($rows.' rows deleted.');
}
/**
* Return the row_id value(s) to use for the command (if any).
*
* #return array|boolean
*/
protected function getRowIds()
{
if (!$this->initialized) {
$this->errorInit(__METHOD__);
}
return $this->rowIds;
}
/**
* Initializes some class properties.
*
* #param InputInterface $input
*/
protected function init(InputInterface $input)
{
if (!$this->initialized) {
$attributeCode = trim($input->getArgument('attribute_code'));
if ($attributeCode == '') {
throw new \RuntimeException(__('attribute_code is required.'));
} elseif (is_numeric($attributeCode)) {
throw new \RuntimeException(__('attribute_code cannot be numeric.'));
}
$attribute = $this->eavSetup->getAttribute(
Product::ENTITY,
$attributeCode
);
if (!$attribute) {
throw new \RuntimeException(__('Invalid attribute_code "%1"', $attributeCode));
}
$backendType = $attribute['backend_type'];
$allowedTypes = ['datetime','decimal','int','text','varchar'];
if (!in_array($backendType, $allowedTypes)) {
throw new \RuntimeException(__(
'backend_type "%1" is not allowed. Allowed types include: %2',
$backendType,
implode(', ', $allowedTypes)
));
}
$this->tableName = $this->connection->getTableName('catalog_product_entity_'.$backendType);
$this->attributeId = (int) $attribute['attribute_id'];
$store = $this->storeManager->getStore($input->getArgument('store'));
if ($store->getCode() == 'admin') {
throw new \RuntimeException(__('Admin Store is not allowed for this command.'));
}
$this->storeId = (int) $store->getId();
$sku = trim($input->getArgument('sku'));
if ($sku != '') {
$sql = $this->connection->select()
->from($this->connection->getTableName('catalog_product_entity'), 'row_id')
->where('sku = ?', $sku)
;
$rowIds = $this->connection->fetchCol($sql);
if (!$rowIds) {
throw new \RuntimeException(__('Invalid Sku "%1"', $sku));
}
foreach ($rowIds as $k => $v) {
$rowIds[$k] = (int) $v;
}
$this->rowIds = $rowIds;
} else {
$this->rowIds = false;
}
$this->initialized = true;
}
}
/**
* Returns the attribute_id to use for the current command.
*
* #return int
*/
protected function getAttributeId()
{
if (!$this->attributeId) {
$this->errorInit(__METHOD__);
}
return $this->attributeId;
}
/**
* Return the store id to use for the current command.
*
* #param InputInterface $input
*/
protected function getStoreId()
{
if (!$this->storeId) {
$this->errorInit(__METHOD__);
}
return $this->storeId;
}
/**
* Return the qualified table name to use for the current command.
*
* #param InputInterface $input
*/
protected function getTableName()
{
if (!$this->tableName) {
$this->errorInit(__METHOD__);
}
return $this->tableName;
}
/**
* Throws an exception.
*
* #param string $methodName
* #throws \LogicException
*/
protected function errorInit($methodName)
{
throw new \LogicException(
__('Command has not been intialized. Call UseDefaultValue::init() before calling '.$methodName));
;
}
}
Step 2: Enable the module
php -f bin/magento module:enable StackOverflow_Question40177336
Step 3: Utilize your new CLI command.
The command has two required arguments, attribute_code and store. Store can be either the ID or Code. Admin store is not allowed for obvious reasons. The command also has an optional third parameter of SKU if you wish to only target a specific SKU (omitting this applies to all products).
For example, if you wanted to delete all "name" values from the "default" store view your command would be as follows:
php -f bin/magento catalog:product:attributes:use-default-value name default

Related

Using markdown in Mailable: Passing an $environment into the "CommonMarkConverter" constructor is deprecated

What I want to archive: I'd like to create a Zammad ticket using the Zammad-api but also parse markdown.
To do so, I created a custom channel to send a notification to the Zammad Helpdesk system using Zammad's API.
This is the specific class:
<?php
namespace App\Channels;
use Illuminate\Mail\Mailable;
class ZammadMessage extends Mailable
{
/**
* The issuer of the ticket.
*
* #var string
*/
public $from;
/**
* The text content of the message.
*
* #var string
*/
private $content;
public function __construct($from, $content = '')
{
$this->from = $from;
$this->content = $content;
}
public static function create($from = '', $content = '')
{
return new static($from, $content);
}
/**
* Set the text content of the message.
*
* #param $content
*
* #return $this
*/
public function content($content)
{
$this->content = $content;
return $this;
}
public function asMarkdown()
{
$this->build();
$this->body = $this->buildView();
return $this;
}
public function build()
{
return $this->from($this->from)
->markdown('emails.contact.submitted', ['data' => $this->content]);
}
/**
* Set the issuer of the ticket.
*
* #param $address
* #param string $name
*
* #return $this
*/
public function from($address, $name = 'null'): static
{
$this->from = $address;
return $this;
}
}
Using this class by my notification class
public function toTicket($notifiable)
{
$address = $notifiable instanceof AnonymousNotifiable
? collect($notifiable->routeNotificationFor('zammad'))->first()
: $notifiable->email;
return ZammadMessage::create()
->from($address)
->content($this->content)
->asMarkdown();
}
I am getting this error:
PHP Deprecated: Passing an $environment into the "League/CommonMark/CommonMarkConverter" constructor is deprecated in 1.6 and will not be supported in 2.0; use MarkdownConverter instead. See https://commonmark.thephpleague.com/2.0/upgrading/consumers/#commonmarkconverter-and-githubflavoredmarkdownconverter-constructors for more details. in /var/www/html/vendor/league/commonmark/src/CommonMarkConverter.php on line 43
That E_USER_DEPRECATED error is not stopping code execution so you should be able to ignore it. Double-check your error_reporting setting in php.ini and/or any similar settings in your framework and adjust as needed.

Laravel - Query scopes across models

In a nutshell, I want to create a function that my query scopes can use across multiple models:
public function scopeNormaliseCurrency($query,$targetCurrency) {
return $query->normaliseCurrencyFields(
['cost_per_day','cost_per_week'],
$targetCurrency
);
}
I have got my logic working within this scope function no problem, but I want to make this code available to all my models, as there are multiple currency fields in different tables and I don't want to be replicating the code in each query scope - only specify the columns that need attention.
So, where would I make my function normaliseCurrencyFields? I have extended the Model class as well as used the newCollection keyword to extend Collection but both result in Call to undefined method Illuminate\Database\Query\Builder::normaliseCurrencyFields() errors.
I have looked into Global Scoping but this seems to be localised to a Model.
Am I along the right lines? Should I be targeting Eloquent specifically?
Create an abstract base model that extends eloquent then extend it with the classes you want to have access to it. I do this for searching functions, uuid creation, and class code functions. So that all of my saved models are required to have to certain attributes and access to my searching functions. For instance I created a static search function getobjectbyid(). So that when extended I can call it like so:
$user = User::getobjectbyid('habwiifnbrklsnbbd1938');
Thus way I know I am getting a user object back.
My base model:
<?php
/**
* Created by PhpStorm.
* User: amac
* Date: 6/5/17
* Time: 12:45 AM
*/
namespace App;
use Illuminate\Database\Eloquent\Model as Eloquent;
abstract class Model extends Eloquent
{
protected $guarded = [
'class_code',
'id'
];
public $primaryKey = 'id';
public $incrementing = false;
public function __construct($attributes = array()) {
parent::__construct($attributes); // Eloquent
$this->class_code = \App\Enums\EnumClassCode::getValueByKey(get_class($this));
$this->id = $this->class_code . uniqid();
return $this;
}
public static function getObjectById($id){
$class = get_called_class();
$results = $class::find($id);
return $results;
}
public static function getAllObjects(){
$class = get_called_class();
return $class::all();
}
my user model:
<?php
namespace App;
use Mockery\Exception;
use Illuminate\Support\Facades\Hash;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use App\Model as Model;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'contact', 'username', 'email_address'
];
/**
* The column name of the "remember me" token.
*
* #var string
*/
protected $rememberTokenName = 'remember_token';
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'remember_token', 'active'
];
/**
* the attributes that should be guarded from Mass Assignment
*
* #var array
*/
protected $guarded = [
'created_at', 'updated_at', 'password_hash'
];
/**
* Define table to be used with this model. It defaults and assumes table names will have an s added to the end.
*for instance App\User table by default would be users
*/
protected $table = "user";
/**
* We have a non incrementing primary key
*
* #var bool
*/
public $incrementing = false;
/**
* relationships
*/
public function contact(){
// return $this->hasOne(Contact::class, 'id', 'contact_id');
return $this->hasOne(Contact::class);
}
public function customers(){
// return $this->hasOne(Contact::class, 'id', 'contact_id');
return $this->hasMany(Customer::class);
}
/**
* User constructor.
* #param array $attributes
*/
public function __construct($attributes = array()) {
parent::__construct($attributes); // Eloquent
// Your construct code.
$this->active = 1;
return $this;
}
/**
* #param $password string
* set user password_hash
* #return $this
*/
public function setPassword($password){
// TODO Password Validation
try{
$this->isActive();
$this->password_hash = Hash::make($password);
$this->save();
} catch(\Exception $e) {
dump($e->getMessage());
}
return $this;
}
/**
* Returns whether or not this use is active.
*
* #return bool
*/
public function isActive(){
if($this->active) {
return true;
} else {
Throw new Exception('This user is not active. Therefore you cannot change the password', 409);
}
}
public function getEmailUsername(){
$contact = Contact::getObjectById($this->contact_id);
$email = Email::getObjectById($contact->email_id);
return $email->username_prefix;
}
/**
* #return string
*
* getFullName
* returns concatenated first and last name of user.
*/
public function getFullName(){
return $this->first_name . ' ' . $this->last_name;
}
/**
* Get the name of the unique identifier for the user.
*
* #return string
*/
public function getAuthIdentifierName(){
return $this->getKeyName();
}
/**
* Get the unique identifier for the user.
*
* #return mixed
*/
public function getAuthIdentifier(){
return $this->{$this->getAuthIdentifierName()};
}
/**
* Get the password for the user.
*
* #return string
*/
public function getAuthPassword(){
return $this->password_hash;
}
/**
* Get the token value for the "remember me" session.
*
* #return string
*/
public function getRememberToken(){
if (! empty($this->getRememberTokenName())) {
return $this->{$this->getRememberTokenName()};
}
}
/**
* Set the token value for the "remember me" session.
*
* #param string $value
* #return void
*/
public function setRememberToken($value){
if (! empty($this->getRememberTokenName())) {
$this->{$this->getRememberTokenName()} = $value;
}
}
/**
* Get the column name for the "remember me" token.
*
* #return string
*/
public function getRememberTokenName(){
return $this->rememberTokenName;
}
/**
* Get the e-mail address where password reset links are sent.
*
* #return string
*/
public function getEmailForPasswordReset(){
}
/**
* Send the password reset notification.
*
* #param string $token
* #return void
*/
public function sendPasswordResetNotification($token){
}
public function validateAddress(){
}
}
a TestController:
public function test(){
$user = User::getObjectById('USR594079ca59746');
$customers = array();
foreach ($user->customers as $customer){
$contact = Contact::getObjectById($customer->contact_id);
$name = PersonName::getObjectById($contact->personname_id);
$c = new \stdClass();
$c->id = $customer->id;
$c->name = $name->preferred_name;
$customers[] = $c;
}
$response = response()->json($customers);
return $response;
}
Take note on how getObjectById is extended and available to my other classes that extend my base model. Also I do not have to specify in my user model an 'id' or 'class_code' and when my user model is constructed it calls the parent constructor which is the constructor on my base model that handles 'id' and 'class_code'.

What is the best way for reusable values throughout the application in Symfony 3?

I want to have a file or list that I can update easily with values that might change throughout my application.
I don't really want to hard code text values into the templates. I prefer to have all of these values in one place and labelled correctly.
Examples of values that might get updated are:
Page title
Logo text
Brand or company name
I have thought about two options:
Add them to the twig config in config.yml. This is a bit messy and doesn't seem organised if I decide to put a lot of values there.
Make a database table for these and include the entity in each controller where I need to use the values. This might be creating too much work.
Are there any other options or are one of these more suitable?
Thank you.
You need to create a twig function and use it to return the value you want. For example:
namespace AppBundle\Twig;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
class TwigExtension extends \Twig_Extension implements ContainerAwareInterface
{
use ContainerAwareTrait;
/**
* #var ContainerInterface
*/
protected $container;
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('parameter', function($name)
{
try {
return $this->container->getParameter($name);
} catch(\Exception $exception) {
return "";
}
})
);
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'app.twig.extension';
}
}
This will create a function called parameter and once you call it in twig {{ parameter('my.parameter') }} it will return the parameter. You need to load it as a service, which you can do by adding the following to your services.yml file:
app.twig.extension:
class: AppBundle\Twig\TwigExtension
calls:
- [setContainer, ["#service_container"]]
tags:
- { name: twig.extension }
From personal experience people usually want to be able to change some of the parameters. This is why I usually prefer to create a Setting or Parameter entity which would look something like this:
/**
* Setting
*
* #ORM\Table(name="my_parameters")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ParameterRepository")
*/
class Parameter
{
/**
* #var integer
*
* #ORM\Id
* #ORM\Column(name="parameter_id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="value", type="text", nullable=true)
*/
private $value;
/**
* #param string|null $name
* #param string|null $value
*/
public function __construct($name = null, $value = null)
{
$this->setName($name);
$this->setValue($value);
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Parameter
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set value
*
* #param string $value
*
* #return Parameter
*/
public function setValue($value = null)
{
$this->value = serialize($value);
return $this;
}
/**
* Get value
*
* #return string
*/
public function getValue()
{
$data = #unserialize($this->value);
return $this->value === 'b:0;' || $data !== false ? $this->value = $data : null;
}
}
Then I would add a CompilerPass which will help get all of the parameters from the database and cache them so that your app doesn't make unnecessary sql queries to the database. That might look something similar to the following class:
// AppBundle/DependencyInjection/Compiler/ParamsCompilerPass.php
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ParamsCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$em = $container->get('doctrine.orm.default_entity_manager');
$settings = $em->getRepository('AppBundle:Parameter')->findAll();
foreach($settings as $setting) {
// I like to prefix the parameters with "app."
// to avoid any collision with existing parameters.
$container->setParameter('app.'.strtolower($setting->getName()), $setting->getValue());
}
}
}
And finally, in your bundle class (i.e. src/AppBundle/AppBundle.php) you add the compiler pass:
namespace AppBundle;
use AppBundle\DependencyInjection\Compiler\ParamsCompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $builder)
{
parent::build($builder);
$builder->addCompilerPass(new ParamsCompilerPass(), , PassConfig::TYPE_AFTER_REMOVING);
}
}
Now you can create a DoctrineFixture template to load the parameters you use all the time. With the TwigExtension you will still be able to call the parameter from the twig template and you can create a web UI to change some of the parameters/settings.

TYPO3 7.6 - Add a public function to the controller

I just try to create my first extension about flowers with a list view and and a detail view. Now I want to add the possibility to browse through the flowers on detail view.
I found the following code Extbase Repository: findNext und findPrevious Funktionen
and added it to my repository
/**
* The repository for Pflanzens
*/
class PflanzenRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
protected $defaultOrderings = array(
'nameDeutsch' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING
);
/**
* Find next item by uid
* #param integer $uid The uid of the current record
* #return boolean|\TYPO3\CMS\Extbase\Persistence\Generic\QueryResult
*/
public function findNext($uid) {
$query = $this->createQuery();
$result = $query->matching($query->greaterThan('uid',$uid))->setLimit(1)->execute();
if($query->count()) {
return $result;
} else {
return false;
}
}
/**
* Find previous item by uid
* #param integer $uid The uid of the current record
* #return boolean|\TYPO3\CMS\Extbase\Persistence\Generic\QueryResult
*/
public function findPrev($uid) {
$query = $this->createQuery();
$ordering = array('uid'=>\TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING);
$result = $query->matching($query->lessThan('uid',$uid))->setLimit(1)->setOrderings($ordering)->execute();
if($query->count()) {
return $result;
} else {
return false;
}
}
}
This is my controller right now:
/**
* PflanzenController
*/
class PflanzenController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{
/**
* pflanzenRepository
*
* #var \TMRuebe\Faerbepflanzen\Domain\Repository\PflanzenRepository
* #inject
*/
protected $pflanzenRepository = NULL;
/**
* action list
*
* #return void
*/
public function listAction()
{
$pflanzens = $this->pflanzenRepository->findAll();
$this->view->assign('pflanzens', $pflanzens);
}
/**
* action show
*
* #param \TMRuebe\Faerbepflanzen\Domain\Model\Pflanzen $pflanzen
* #return void
*/
public function showAction(\TMRuebe\Faerbepflanzen\Domain\Model\Pflanzen $pflanzen)
{
$this->view->assign('pflanzen', $pflanzen);
}
}
Now I need help how to add the two public functions to the controller. And I also need a hint for the variable that I can use in my fluid template to create the previous link and the next link.
in showAction() you need to assign to further variables with the results of findNext() and findPrev().
$this->view->assign('previous', \TMRuebe\Faerbepflanzen\Domain\Repository\PflanzenRepository::findPrev($pflanzen['uid']));
$this->view->assign('next', \TMRuebe\Faerbepflanzen\Domain\Repository\PflanzenRepository::findNext($pflanzen['uid']));
in your detail template you need to build the links like the links in the list view.
You might build methods using the current object to get easier access to next and prev.

Magento 2.1 Custom module relationship

I developed few custom modules for my Magento 2.1 store for smart manegmant of content in some of the CMS pages.
I used this tutorial https://www.ashsmith.io/magento2/module-from-scratch-introduction/ and this example https://github.com/ashsmith/magento2-blog-module-tutorial in order to do it.
Now, I have on page with list of FAQ, but each FAQ is belongs to FAQ Category (Not the Catalog Category).
So there is two custom modules here (FAQ Category and FAQ Question).
The FAQ Category only have Title field.
The FAQ Question have Title field, Answer (text editor) field, and FAQ Question dropdown (select box with list of all availble FAQ categories)
I don't know how to achive this.
What is the right way to do it? Especially the admin part.
I assume you want to join fields. You cannot do this by using virtual type in di.xml, so you need to follow these steps and update your files
#File etc/di.xml
<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
<arguments>
<argument name="collections" xsi:type="array">
<item name="namespace_modulename_listing_data_source" xsi:type="string">Namespace\Modulename\Model\Resource\Modulename\Grid\Collection</item>
</argument>
</arguments>
</type>
<type name="Namespace\Modulename\Model\Resource\Modulename\Grid\Collection">
<arguments>
<argument name="mainTable" xsi:type="string">tablename</argument>
<argument name="eventPrefix" xsi:type="string">namespace_modulename_grid_collection</argument>
<argument name="eventObject" xsi:type="string">namespace_grid_collection</argument>
<argument name="resourceModel" xsi:type="string">Namespace\Modulename\Model\Resource\Modulename</argument>
</arguments>
</type>
In Your Resource Model File Model/Resource/Modulename/Collection.php
<?php
namespace Namespace\Modulename\Model\Resource\Modulename;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
class Collection extends AbstractCollection
{
/**
* Define model & resource model
*/
const YOUR_TABLE = 'tablename';
public function __construct(
\Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory,
\Psr\Log\LoggerInterface $logger,
\Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
\Magento\Framework\Event\ManagerInterface $eventManager,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
\Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null
) {
$this->_init(
'Namespace\Modulename\Model\Modulename',
'Namespace\Modulename\Model\Resource\Modulename'
);
parent::__construct(
$entityFactory, $logger, $fetchStrategy, $eventManager, $connection,
$resource
);
$this->storeManager = $storeManager;
}
protected function _initSelect()
{
parent::_initSelect();
$this->getSelect()->joinLeft(
['join_table' => $this->getTable('tablename')],
'main_table.columnname = join_table.column_name',
'*'
);
}
}
?>
Now your Model/Resource/ModuleName/Grid/Collection.php
<?php
namespace Namespace\Modulename\Model\Resource\Modulename\Grid;
use Magento\Framework\Api\Search\SearchResultInterface;
use Magento\Framework\Search\AggregationInterface;
use Namespace\Modulename\Model\Resource\Modulename\Collection as ModulenameCollection;
/**
* Class Collection
* Collection for displaying grid
*/
class Collection extends ModulenameCollection implements SearchResultInterface
{
/**
* Resource initialization
* #return $this
*/
public function __construct(
\Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory,
\Psr\Log\LoggerInterface $logger,
\Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
\Magento\Framework\Event\ManagerInterface $eventManager,
\Magento\Store\Model\StoreManagerInterface $storeManager,
$mainTable,
$eventPrefix,
$eventObject,
$resourceModel,
$model = 'Magento\Framework\View\Element\UiComponent\DataProvider\Document',
$connection = null,
\Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null
) {
parent::__construct(
$entityFactory,
$logger,
$fetchStrategy,
$eventManager,
$storeManager,
$connection,
$resource
);
$this->_eventPrefix = $eventPrefix;
$this->_eventObject = $eventObject;
$this->_init($model, $resourceModel);
$this->setMainTable($mainTable);
}
/**
* #return AggregationInterface
*/
public function getAggregations()
{
return $this->aggregations;
}
/**
* #param AggregationInterface $aggregations
*
* #return $this
*/
public function setAggregations($aggregations)
{
$this->aggregations = $aggregations;
}
/**
* Get search criteria.
*
* #return \Magento\Framework\Api\SearchCriteriaInterface|null
*/
public function getSearchCriteria()
{
return null;
}
/**
* Set search criteria.
*
* #param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
*
* #return $this
* #SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function setSearchCriteria(
\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria = null
) {
return $this;
}
/**
* Get total count.
*
* #return int
*/
public function getTotalCount()
{
return $this->getSize();
}
/**
* Set total count.
*
* #param int $totalCount
*
* #return $this
* #SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function setTotalCount($totalCount)
{
return $this;
}
/**
* Set items list.
*
* #param \Magento\Framework\Api\ExtensibleDataInterface[] $items
*
* #return $this
* #SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function setItems(array $items = null)
{
return $this;
}
}
?>
Now you can use the join table columns any where in your grid and when you call collection.

Resources