Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Dokan Data Store #2346

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ea2a6e9
feat: Dokan Data Store
shohag121 Aug 21, 2024
1628844
Add base Model
mrabbani Oct 21, 2024
40e5ecb
Add vendor Model
mrabbani Oct 22, 2024
a216c96
Add Test case
mrabbani Oct 23, 2024
7e681f9
Add test case for datastore
mrabbani Oct 23, 2024
f583f2c
Add test case for CRUD
mrabbani Oct 24, 2024
73b289d
Cast Date object to mysql date formate
mrabbani Oct 24, 2024
c8b207c
Add developer docs
mrabbani Oct 25, 2024
aae536f
Add delete_by method
mrabbani Oct 25, 2024
9b5aa1c
Add update_by to base store
mrabbani Oct 29, 2024
80b6bee
Add general update query method in Data Sotre
mrabbani Nov 6, 2024
fc33dc8
Update vendor balance Model and Store to perform update operations
mrabbani Nov 6, 2024
a07efbf
Refactor dokan order sync for vendor balance
mrabbani Nov 6, 2024
afaeb93
Merge branch 'develop' into feat/data-store-and-models
mrabbani Nov 6, 2024
be45417
Merge with refactor/vendor-balance-raw-query
mrabbani Nov 6, 2024
a47f411
Add database mission assertion method for test case
mrabbani Nov 7, 2024
f57f2fa
Add read_meta method to BaseStore
mrabbani Nov 7, 2024
7d4b345
Add method to get vendor total balance
mrabbani Nov 7, 2024
d348e0c
Fetch vendor balance with the method of VendorBalance Method
mrabbani Nov 7, 2024
45965a3
Refactor/vendor balance raw query (#2430)
mrabbani Nov 8, 2024
a8ce67d
Rename method get_total_balance_by_vendor to test_get_total_earning_b…
mrabbani Nov 8, 2024
59ea07f
Update docs blocks
mrabbani Nov 8, 2024
d504acb
Fix merge conflict
mrabbani Nov 8, 2024
b950b9c
Bind model to Service provider
mrabbani Nov 12, 2024
774843d
Get model from the container to perform operation
mrabbani Nov 12, 2024
8417e49
Remove empty lines
mrabbani Nov 13, 2024
cb6b364
Add data provider for test case
mrabbani Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions docs/data-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
## Dokan Model & Data Store Documentation

- [Data Model](#data-model)
- [Data Store](#data-store)
- [Uses of Models](#uses-of-models)

## Data Model

### Overview
The Dokan Data Model class should follow a structure similar to the `WC_Product` class, where the data layer is abstracted through the **Data Store**. This separation ensures that the model class is used throughout the Dokan plugin without interacting directly with the database or Data Store.

### Goal
The goal is to minimize the use of raw queries and enhance performance across various parts of the plugin. The data model should efficiently manage storing and retrieving data from the database.

### Implementation
The model class should extend the [\WeDevs\Dokan\Models\BaseModel](../includes/Models/BaseModel.php) class. It includes the following key properties and methods:

#### Properties
- `protected $object_type`: The type of object, such as `product`.
- `protected $data`: Holds the default data for the object.

#### Methods
- The `protected $data_store` property is initialized in the constructor.
- Getter and Setter methods use the `get_{key}` and `set_{key}` conventions, leveraging the `get_prop( $prop_name, $context )` and `set_prop( $prop_name, $prop_value )` methods for interacting with data properties.

Below is a sample class implementation:

```php
class Department extends \WeDevs\Dokan\Models\BaseModel {
protected $object_type = 'department';

protected $data = array(
'name' => '',
'date_created' => null,
'date_updated' => null,
);

/**
* Initialize the data store
*/
public function __construct( int $item_id = 0 ) {
parent::__construct( $item_id );
$this->data_store = new \WeDevs\Dokan\Models\DataStore\DepartmentStore();

if ( $this->get_id() > 0 ) {
$this->data = $this->data_store->read( $this );
}
}

public function get_name( $context = 'view' ) {
return $this->get_prop('name', $context);
}

public function set_name( $name ) {
return $this->set_prop( 'name', $name );
}

/**
* @return \WC_DateTime|NULL Since the value was set by `set_date_prop` method
*/
public function get_date_created( $context = 'view' ) {
return $this->get_prop('date_created', $context);
}

/**
* Set the date type value using the `set_date_prop` method.
*/
public function set_date_created( $date_created ) {
return $this->set_date_prop( 'date_created', $date_created );
}

/**
* @return \WC_DateTime|NULL Since the value was set by `set_date_prop` method
*/
public function get_date_updated( $context = 'view' ) {
return $this->get_prop('date_updated', $context);
}

public function set_date_updated( $date_updated ) {
return $this->set_date_prop( 'date_updated', $date_updated );
}

/**
* Set the updated date for the entity.
*
* This method is protected to ensure that only internal code like `set_props` method
* can set the updated date dynamically. External clients should use
* the `set_date_created` and `set_date_updated` methods to manage dates semantically.
*
* @param string|int|DateTime $date_created
*/
protected function set_updated_at( $date_updated ) {
$this->set_date_updated( $date_updated );
}
}
```

## Data Store

- [Override DB Date Format](#override-db-date-format)
- [Customizing the ID Field](#customizing-the-id-field)

Your data store class should extend the [\WeDevs\Dokan\Models\DataStore\BaseDataStore](../includes/Models/DataStore/BaseDataStore.php) class and must implement the following methods:

- `get_table_name`: Defines the table name in the database.
- `get_fields_with_format`: Returns the fields with format as an array where key is the db field name and value is the format..

Sample implementation:

```php
class DepartmentStore extends \WeDevs\Dokan\Models\DataStore\BaseDataStore {
public function get_table_name(): string {
return 'dokan_department';
}

public function get_fields_with_format(): array {
return [
'name' => '%s',
'date_created' => '%s',
'updated_at' => '%s',
];
}

public function get_date_updated( $context = 'view' ) {
return $this->get_prop('date_updated', $context);
}
}
```
Comment on lines +111 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix implementation issues in DepartmentStore class.

The DepartmentStore implementation has the following issues:

  1. The get_date_updated method appears to be incorrectly placed in the store class instead of the model class
  2. Missing return type hints for better type safety

Apply these changes:

 class DepartmentStore extends \WeDevs\Dokan\Models\DataStore\BaseDataStore {
-    public function get_table_name(): string {
+    public function get_table_name(): string {
         return 'dokan_department';
     }

-    public function get_fields_with_format(): array {
+    public function get_fields_with_format(): array {
         return [
             'name' => '%s',
             'date_created' => '%s',
             'updated_at' => '%s',
         ];
     }
-
-    public function get_date_updated( $context = 'view' ) {
-        return $this->get_prop('date_updated', $context);
-    }
 }

Committable suggestion was skipped due to low confidence.


### Override DB Date Format

The Data Store first checks for the existence of the `get_{field_name}` method to map the data during insert or update operations. There are two ways to override the DB date format:

**Option 1:**
Override the `get_date_format_for_field` method to specify a custom format.
```php
protected function get_date_format_for_field( string $db_field_name ): string {
return 'Y-m-d';
}
```

**Option 2:**
Create a custom method like `get_{db_field}` for more control over date formatting:
```php
protected function get_updated_at( Department $model, $context = 'edit' ): string {
return $model->get_date_updated( $context )->date('Y-m-d');
}
```
> **Option 2** could be used to map the data during insert or update operations when Model's data key and Store's data key are different.

### Customizing the ID Field

To customize the name and format of the ID field in the database, override the `get_id_field_name` and `get_id_field_format` methods. By default, the ID field is set to `id` and format is `%d`.

## Uses of Models

### Create a New Record
```php
$department = new Department();
$department->set_name('Department 1');
$department->save();
```

### Read a Record
```php
$department = new Department( $department_id );
echo $department->get_name();
```

### Update a Record
```php
$department = new Department( $department_id );
$department->set_name('Department 2');
$department->save();
```

### Case Study
Pls check the example [get_particulars](../includes/Models/VendorBalance.php#L16) & [set_perticulars](../includes/Models/VendorBalance.php#L146) methods of Model and [get_perticulars](../includes/Models/DataStore/VendorBalanceStore.php#L35) method of Data Store.

`perticulars` field name in database is *typo* but I don't want to expose public method `get_perticulars` in Model class instead of `get_particulars`.

We could do the same thing by overriding the following methods in Data Store.
- `protected function map_model_to_db_data( BaseModel &$model ): array`: Prepare data for saving a BaseModel to the database.
- `map_db_raw_to_model_data`: Maps database raw data to model data.
17 changes: 8 additions & 9 deletions includes/Commission.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use WC_Order;
use WC_Product;
use WeDevs\Dokan\Models\VendorBalance;
use WeDevs\Dokan\ProductCategory\Helper;
use WeDevs\DokanPro\Modules\DeliveryTime\StorePickup\Vendor;
use WP_Error;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;

Expand Down Expand Up @@ -97,15 +99,12 @@
[ '%d' ]
);

$wpdb->update(
$wpdb->dokan_vendor_balance,
[ 'debit' => (float) $net_amount ],
[
'trn_id' => $tmp_order->get_id(),
'trn_type' => 'dokan_orders',
],
[ '%f' ],
[ '%d', '%s' ]
$vendor_balance_model = dokan()->get_container()->get( VendorBalance::class );

$vendor_balance_model->update_by_transaction(
$tmp_order->get_id(),
'dokan_orders',
[ 'debit' => (float) $net_amount ]
);

$tmp_order->update_meta_data( 'dokan_gateway_fee', $gateway_fee );
Expand Down Expand Up @@ -566,7 +565,7 @@
*
* @return float | null on failure
*/
public function prepare_for_calculation( $callable, $product_id = 0, $product_price = 0 ) {

Check warning on line 568 in includes/Commission.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

It is recommended not to use reserved keyword "callable" as function parameter name. Found: $callable
do_action( 'dokan_before_prepare_for_calculation', $callable, $product_id, $product_price, $this );

// If an order has been purchased previously, calculate the earning with the previously stated commission rate.
Expand Down
38 changes: 38 additions & 0 deletions includes/DependencyManagement/Providers/ModelServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace WeDevs\Dokan\DependencyManagement\Providers;

use WeDevs\Dokan\DependencyManagement\BaseServiceProvider;

class ModelServiceProvider extends BaseServiceProvider {
/**
* Tag for services added to the container.
*/
public const TAG = 'ajax-service';

protected $services = [
\WeDevs\Dokan\Models\VendorBalance::class,
\WeDevs\Dokan\Models\DataStore\VendorBalanceStore::class,
];

/**
* {@inheritDoc}
*
* Check if the service provider can provide the given service alias.
*
* @param string $alias The service alias to check.
* @return bool True if the service provider can provide the service, false otherwise.
*/
public function provides( string $alias ): bool {
return in_array( $alias, $this->services, true );
}

/**
* Register the classes.
*/
public function register(): void {
foreach ( $this->services as $service ) {
$this->getContainer()->add( $service, $service );
}
}
Comment on lines +33 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding error handling and service validation.

The register() method could be more robust with:

  1. Error handling for invalid service classes
  2. Verification if service is already registered
  3. More descriptive documentation including @throws annotation
     /**
      * Register the classes.
+     *
+     * @throws \Exception If a service class does not exist
      */
     public function register(): void {
         foreach ( $this->services as $service ) {
+            if (!class_exists($service)) {
+                throw new \Exception("Service class {$service} does not exist");
+            }
+            if ($this->getContainer()->has($service)) {
+                continue;
+            }
             $this->getContainer()->add( $service, $service );
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function register(): void {
foreach ( $this->services as $service ) {
$this->getContainer()->add( $service, $service );
}
}
/**
* Register the classes.
*
* @throws \Exception If a service class does not exist
*/
public function register(): void {
foreach ( $this->services as $service ) {
if (!class_exists($service)) {
throw new \Exception("Service class {$service} does not exist");
}
if ($this->getContainer()->has($service)) {
continue;
}
$this->getContainer()->add( $service, $service );
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public function boot(): void {
$this->getContainer()->addServiceProvider( new FrontendServiceProvider() );
$this->getContainer()->addServiceProvider( new AjaxServiceProvider() );
$this->getContainer()->addServiceProvider( new AnalyticsServiceProvider() );
$this->getContainer()->addServiceProvider( new ModelServiceProvider() );
}

/**
Expand Down
104 changes: 104 additions & 0 deletions includes/Models/BaseModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace WeDevs\Dokan\Models;

use WC_Data;

abstract class BaseModel extends WC_Data {
/**
* Save should create or update based on object existence.
*
* @return int
*/
public function save() {
// wc_get_product()
if ( ! $this->data_store ) {
return $this->get_id();
}

/**
* Trigger action before saving to the DB. Allows you to adjust object props before save.
*
* @param WC_Data $this The object being saved.
* @param WC_Data_Store_WP $data_store THe data store persisting the data.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct capitalization in parameter descriptions.

In the docblocks for $data_store at lines 23 and 37, "THe data store persisting the data." should be "The data store persisting the data."

Apply this diff to fix the typos:

-		 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
+		 * @param WC_Data_Store_WP $data_store The data store persisting the data.

Also applies to: 37-37

*/
do_action( 'dokan_before_' . $this->object_type . '_object_save', $this, $this->data_store );

if ( $this->get_id() ) {
$this->data_store->update( $this );
} else {
$this->data_store->create( $this );
}

/**
* Trigger action after saving to the DB.
*
* @param WC_Data $this The object being saved.
* @param WC_Data_Store_WP $data_store THe data store persisting the data.
*/
do_action( 'dokan_after_' . $this->object_type . '_object_save', $this, $this->data_store );

return $this->get_id();
}

/**
* Delete an object, set the ID to 0, and return result.
*
* @param bool $force_delete Should the date be deleted permanently.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix typo in parameter description of delete() method.

In the docblock for the delete() method, "Should the date be deleted permanently." should be "Should the data be deleted permanently."

Apply this diff to correct the typo:

-	 * @param  bool $force_delete Should the date be deleted permanently.
+	 * @param  bool $force_delete Should the data be deleted permanently.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param bool $force_delete Should the date be deleted permanently.
* @param bool $force_delete Should the data be deleted permanently.

* @return bool result
*/
public function delete( $force_delete = false ) {
/**
* Filters whether an object deletion should take place. Equivalent to `pre_delete_post`.
*
* @param mixed $check Whether to go ahead with deletion.
* @param Data $this The data object being deleted.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistency in parameter types in docblocks.

At line 55, the parameter type for $this is listed as Data. To maintain consistency and accuracy, it should be WC_Data, matching the type used in other methods.

Apply this diff to correct the parameter type:

-		 * @param Data $this The data object being deleted.
+		 * @param WC_Data $this The data object being deleted.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param Data $this The data object being deleted.
* @param WC_Data $this The data object being deleted.

* @param bool $force_delete Whether to bypass the trash.
*
* @since 8.1.0.
*/
$check = apply_filters( "dokan_pre_delete_$this->object_type", null, $this, $force_delete );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix variable interpolation in apply_filters call.

When interpolating object properties within double-quoted strings, you need to enclose the property in braces to ensure correct parsing. Without braces, PHP may not parse the variable correctly.

Apply this diff to fix the variable interpolation:

-		$check = apply_filters( "dokan_pre_delete_$this->object_type", null, $this, $force_delete );
+		$check = apply_filters( "dokan_pre_delete_{$this->object_type}", null, $this, $force_delete );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$check = apply_filters( "dokan_pre_delete_$this->object_type", null, $this, $force_delete );
$check = apply_filters( "dokan_pre_delete_{$this->object_type}", null, $this, $force_delete );


if ( null !== $check ) {
return $check;
}

if ( $this->data_store ) {
$this->data_store->delete( $this, array( 'force_delete' => $force_delete ) );
$this->set_id( 0 );
return true;
}

return false;
}

/**
* Delete raws from the database.
*
* @param array $data Array of args to delete an object, e.g. `array( 'id' => 1, status => ['draft', 'cancelled'] )` or `array( 'id' => 1, 'status' => 'publish' )`.
* @return bool result
*/
public static function delete_by( array $data ) {
$object = new static();
return $object->data_store->delete_by( $data );
}
Comment on lines +81 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance delete_by method with better type safety and error handling.

The static delete_by method needs:

  1. Return type hint
  2. Error handling for data store operations
  3. Documentation for possible exceptions
+	/**
+	 * Delete rows from the database.
+	 *
+	 * @param array $data Array of args to delete objects.
+	 * @return bool True on success, false on failure.
+	 * @throws \Exception When data store operation fails.
+	 */
-	public static function delete_by( array $data ) {
+	public static function delete_by( array $data ): bool {
 		$object = new static();
+		try {
 			return $object->data_store->delete_by( $data );
+		} catch ( \Exception $e ) {
+			wc_doing_it_wrong( __FUNCTION__, $e->getMessage(), '3.0.0' );
+			return false;
+		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static function delete_by( array $data ) {
$object = new static();
return $object->data_store->delete_by( $data );
}
/**
* Delete rows from the database.
*
* @param array $data Array of args to delete objects.
* @return bool True on success, false on failure.
* @throws \Exception When data store operation fails.
*/
public static function delete_by( array $data ): bool {
$object = new static();
try {
return $object->data_store->delete_by( $data );
} catch ( \Exception $e ) {
wc_doing_it_wrong( __FUNCTION__, $e->getMessage(), '3.0.0' );
return false;
}
}


/**
* Prefix for action and filter hooks on data.
*
* @return string
*/
protected function get_hook_prefix() {
return 'dokan_' . $this->object_type . '_get_';
}

/**
* Get All Meta Data.
*
* @since 2.6.0
* @return array of objects.
*/
public function get_meta_data() {
return apply_filters( $this->get_hook_prefix() . 'meta_data', array() );
}
Comment on lines +101 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Review meta data implementation.

The current implementation seems incomplete:

  1. It returns an empty array by default without actually retrieving meta data
  2. It doesn't utilize WC_Data's meta data functionality

Consider implementing proper meta data retrieval:

 	public function get_meta_data() {
-		return apply_filters( $this->get_hook_prefix() . 'meta_data', array() );
+		$meta_data = parent::get_meta_data();
+		return apply_filters( $this->get_hook_prefix() . 'meta_data', $meta_data );
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function get_meta_data() {
return apply_filters( $this->get_hook_prefix() . 'meta_data', array() );
}
public function get_meta_data() {
$meta_data = parent::get_meta_data();
return apply_filters( $this->get_hook_prefix() . 'meta_data', $meta_data );
}

}
Loading
Loading