Showing posts with label Ec-Cube. Show all posts
Showing posts with label Ec-Cube. Show all posts

Sunday, 29 September 2024

Create a Storage System Like Laravel’s in EC-CUBE

Here’s the complete implementation of a storage system in EC-CUBE 4.2 that allows you to switch between storage drivers (Local, S3, Cloudinary) using an interface, similar to Laravel's storage system.

1. Update Your .env File

Set the default storage driver in your .env file:

STORAGE_DRIVER=local # Change to "s3" or "cloudinary" as needed

2. Create the Storage Interface

Create a new interface for your storage system.

// src/Service/StorageInterface.php namespace App\Service; interface StorageInterface { public function put(string $path, $contents); public function get(string $path); public function delete(string $path); public function url(string $path); }

3. Implement Storage Drivers

Local Storage Driver

// src/Service/Drivers/LocalStorageDriver.php namespace App\Service\Drivers; use App\Service\StorageInterface; class LocalStorageDriver implements StorageInterface { protected $root; public function __construct($root) { $this->root = $root; } public function put(string $path, $contents) { return file_put_contents($this->root . '/' . $path, $contents); } public function get(string $path) { return file_get_contents($this->root . '/' . $path); } public function delete(string $path) { return unlink($this->root . '/' . $path); } public function url(string $path) { return '/storage/' . $path; // Adjust this according to your URL structure } }

S3 Storage Driver

// src/Service/Drivers/S3StorageDriver.php namespace App\Service\Drivers; use Aws\S3\S3Client; use App\Service\StorageInterface; class S3StorageDriver implements StorageInterface { protected $client; protected $bucket; public function __construct(array $config) { $this->client = new S3Client([ 'version' => 'latest', 'region' => $config['region'], 'credentials' => [ 'key' => $config['key'], 'secret' => $config['secret'], ], ]); $this->bucket = $config['bucket']; } public function put(string $path, $contents) { $this->client->putObject([ 'Bucket' => $this->bucket, 'Key' => $path, 'Body' => $contents, 'ACL' => 'public-read', ]); return $this->url($path); } public function get(string $path) { $result = $this->client->getObject([ 'Bucket' => $this->bucket, 'Key' => $path, ]); return (string) $result['Body']; } public function delete(string $path) { return $this->client->deleteObject([ 'Bucket' => $this->bucket, 'Key' => $path, ]); } public function url(string $path) { return $this->client->getObjectUrl($this->bucket, $path); } }

Cloudinary Storage Driver

// src/Service/Drivers/CloudinaryStorageDriver.php namespace App\Service\Drivers; use Cloudinary\Cloudinary; use App\Service\StorageInterface; class CloudinaryStorageDriver implements StorageInterface { protected $cloudinary; public function __construct(array $config) { $this->cloudinary = new Cloudinary([ 'cloud' => [ 'cloud_name' => $config['cloud_name'], 'api_key' => $config['api_key'], 'api_secret' => $config['api_secret'], ], ]); } public function put(string $path, $contents) { return $this->cloudinary->uploadApi()->upload($contents, ['public_id' => $path]); } public function get(string $path) { return $this->cloudinary->adminApi()->asset($path)['secure_url']; } public function delete(string $path) { return $this->cloudinary->adminApi()->deleteAssets([$path]); } public function url(string $path) { return $this->cloudinary->adminApi()->asset($path)['secure_url']; } }

4. Create the Storage Manager

Create a class to manage storage operations.

// src/Service/StorageManager.php namespace App\Service; class StorageManager { private $storage; public function __construct(StorageInterface $storage) { $this->storage = $storage; } public function uploadFile(string $path, $contents) { return $this->storage->put($path, $contents); } public function retrieveFile(string $path) { return $this->storage->get($path); } public function deleteFile(string $path) { return $this->storage->delete($path); } public function getFileUrl(string $path) { return $this->storage->url($path); } }

5. Create the Storage Factory

This factory will dynamically return the correct storage driver.

// src/Service/StorageFactory.php namespace App\Service; use Symfony\Component\DependencyInjection\ContainerInterface; class StorageFactory { private $container; public function __construct(ContainerInterface $container) { $this->container = $container; } public function create(): StorageInterface { $driver = getenv('STORAGE_DRIVER') ?: 'local'; // Default to local if not set switch ($driver) { case 's3': return $this->container->get(Drivers\S3StorageDriver::class); case 'cloudinary': return $this->container->get(Drivers\CloudinaryStorageDriver::class); case 'local': default: return $this->container->get(Drivers\LocalStorageDriver::class); } } }

6. Update the Service Configuration

Modify your config/services.yaml to bind the storage interface and set up services.

services: App\Service\StorageInterface: factory: ['@App\Service\StorageFactory', 'create'] App\Service\Drivers\LocalStorageDriver: arguments: $root: '%kernel.project_dir%/var/storage' App\Service\Drivers\S3StorageDriver: arguments: $config: key: '%env(AWS_ACCESS_KEY_ID)%' secret: '%env(AWS_SECRET_ACCESS_KEY)%' region: '%env(AWS_DEFAULT_REGION)%' bucket: '%env(AWS_BUCKET)%' App\Service\Drivers\CloudinaryStorageDriver: arguments: $config: cloud_name: '%env(CLOUDINARY_CLOUD_NAME)%' api_key: '%env(CLOUDINARY_API_KEY)%' api_secret: '%env(CLOUDINARY_API_SECRET)%'

7. Using the Storage Manager

You can now inject StorageManager into your controllers or services and use it as needed:

// src/Controller/YourController.php namespace App\Controller; use App\Service\StorageManager; class YourController { private $storageManager; public function __construct(StorageManager $storageManager) { $this->storageManager = $storageManager; } public function uploadAction() { $filePath = 'uploads/product.jpg'; $fileContents = file_get_contents('path/to/local/file.jpg'); $this->storageManager->uploadFile($filePath, $fileContents); $fileUrl = $this->storageManager->getFileUrl($filePath); // Do something with $fileUrl } }

Summary

This implementation allows you to switch between different storage drivers by changing the STORAGE_DRIVER variable in your .env file without modifying your code. You have a clean interface and the necessary drivers to handle local, S3, and Cloudinary storage in EC-CUBE 4.2.

Feel free to reach out if you have any further questions or need assistance!

Thank you

Saturday, 11 May 2024

Sync & Queued Message Handling in Eccube

1. PHP test connect to Redis

need install "predis/predis"

use Predis\Client;
// Redis server configuration
$host = 'redis';
$port = 6379;
$password = 'secret_redis'; // Replace 'your_password' with your actual password

// Connect to Redis server
try {
$redis = new Client([
'scheme' => 'tcp',
'host' => $host,
'port' => $port,
'password'=> $password
]);
$redis->ping();
echo "Connected to Redis server successfully.";
} catch (Exception $e) {
echo "Failed to connect to Redis server: " . $e->getMessage();
}
 }

2. File /app/config/eccube/packages/messenger.yaml

framework:
messenger:
# reset services after consuming messages
reset_on_message: true

# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
# failure_transport: failed

transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: 'redis://:secret_redis@redis:6379/messages'

routing:
# async is whatever name you gave your transport above
'Customize\Message\SmsNotification': async

# php bin/console debug:messenger
# php bin/console messenger:consume async

And in .env add:

MESSENGER_TRANSPORT_DSN='redis://:{redis_password}@redis:6379/messages'

MESSENGER_TRANSPORT_DSN='redis://:secret_redis@redis:6379/messages'

3. /app/Customize/Message/SmsNotification.php 

<?php

namespace Customize\Message;

class SmsNotification
{
private $content;

public function __construct(string $content)
{
$this->content = $content;
}

public function getContent(): string
{
return $this->content;
}
}

4. /app/Customize/MessageHandler/SmsNotificationHandler.php

<?php
namespace Customize\MessageHandler;

use Customize\Message\SmsNotification;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class SmsNotificationHandler
{
public function __invoke(SmsNotification $message)
{
log_error('HELLO SmsNotificationHandler');
dump($message);
}
}

5. DemoController.php

<?php

/*
* This file is part of EC-CUBE
*
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
*
* http://www.ec-cube.co.jp/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Eccube\Controller;

use Customize\Message\SmsNotification;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;

class TopController extends AbstractController
{
/**
* @Route("/demo", name="demo", methods={"GET"})
*/
public function demo(MessageBusInterface $bus)
{
// will cause the SmsNotificationHandler to be called
$bus->dispatch(new SmsNotification('Look! I created a message!'));
        echo 'DEMO CALL SmsNotification';
 
return new Response(
'',
Response::HTTP_OK,
array('Content-Type' => 'text/plain; charset=utf-8')
);
}
}

* use command

# use to list all jobs
php bin/console debug:messenger
 
# use to run job; add -vv to see details 
php bin/console messenger:consume async
 

Reference:

https://symfony.com/doc/current/messenger.html

Thank you

Friday, 10 May 2024

Customizing FormType in Ec-cube

 FormExtension allows you to customize existing forms. 

1. Specifying the form type to extend

2. Functions for extension 

You can customize the form by overriding the functions below and changing the parameters passed as arguments.  

+ buildForm() 

+ buildView() 

+ configureOptions() 

+ finishView()

Reference:
-
https://doc4.ec-cube.net/customize_formtype

Thank you.

Customizing the repository in Ec-cube

1. QueryBuilder extension

Reference:
- https://doc4.ec-cube.net/customize_repository

Thank you.

Customizing Entities in Ec-cube

1. Expansion method for Enity

2. Use Entity through Repository

3. Automatically generate a form from Entit and customize form

4. Using  $this->entityManger in Controller

Reference:
- https://doc4.ec-cube.net/customize_entity

Thank you.

Customizing Controller in Ec-cube

1. Adding a new route

2. Overwrite existing routing

3. Make a redirect

4. Use services within Controller

+ Services in AbstractController 

+ Services Not in AbstractController  

5. Create a controller that doesn't need to display a screen

Reference:
- https://doc4.ec-cube.net/customize_controller

Thank you.

Thursday, 7 March 2024

Using .twig in Eccube

1. add new variable in .env

APP_URL=http://eccube.test

2. add config to .yml: app\config\eccube\packages\eccube.yaml

    # EC-CUBE default env parameters
    env(APP_URL): 'http://ec-cube-dev.cdb-lab.com'

    # EC-CUBE parameter
    app_url: '%env(APP_URL)%'

3. using in .twig

    {{ eccube_config.app_url }}

4. using in controller

echo $this->eccubeConfig['app_url'];

5. using in service

use Eccube\Common\EccubeConfig;
.... 
/**
*
@var EccubeConfig
*/
 protected $eccubeConfig;
 
public function __construct(EccubeConfig $eccubeConfig) {
$this->eccubeConfig = $eccubeConfig;
// in method using:
echo $this->eccubeConfig['app_url'];

Thank you

Wednesday, 6 March 2024

Using domPdf in Eccube with Svg, Font family

1. Install dompdf

composer require dompdf/dompdf

2. Add Svg to Pdf

Caculate $dataQrs

$dataQrs = ImageHelper::imageToBase64($qrPathProduct)

Function imageToBase64

    public static function imageToBase64($path)
    {
        $path = $path;
        $type = pathinfo($path, PATHINFO_EXTENSION);
        $data = file_get_contents($path);
        $base64 = 'data:image/' . $type . ';base64,' . base64_encode($data);
        return $base64;
    }

3. Custom font family for Pdf 

Custom fontDir, fontCache

    /**
     * Generate QR code pdf
     * @return array<string> images qr
     */
    public function generateQRCodePdf($dataQrs)
    {
        // render pdf from $dataQrs
        $html =  $this->templating->render('@admin/Product/qr_code_template_pdf.twig', ['dataQrs' => $dataQrs]);

        // set option for dompdf
        $options = new Options([
            "rootDir" =>"vendor/dompdf/dompdf",
"fontDir" =>"
var/cache/qr/fonts", // path for auto generate font
"fontCache" =>"
var/cache/qr/fonts", // path for auto generate font
"logOutputFile" =>null,
"defaultMediaType" =>"screen",
"defaultPaperSize" =>"a4",
"defaultPaperOrientation" =>"portrait",
"defaultFont" =>"Noto Sans",
"dpi" =>120,
"fontHeightRatio" =>1.1,
"isPhpEnabled" =>false,
"isRemoteEnabled" =>true, // have to true to generate font
"isJavascriptEnabled" =>true,
"isHtml5ParserEnabled" =>true,
"isFontSubsettingEnabled" =>false,
"debugPng" =>false,
"debugKeepTemp" =>false,
"debugCss" =>false,
"debugLayout" =>false,
"debugLayoutLines" =>true,
"debugLayoutBlocks" =>true,
"debugLayoutInline" =>true,
"debugLayoutPaddingBox" =>true,
"pdfBackend" =>"PDFLib",
"pdflibLicense" =>""
        ]);
        $dompdf = new Dompdf($options);
        $dompdf->loadHtml($html);
        $dompdf->setPaper('A4', 'portrait');
        $dompdf->render();

        // store temp_qr_pdf for this user
        $filePdf = QRCodeSizes::QR_PATH . '/' . $this->handler->getId() . 'qr_temp.pdf';
        $filePath = $this->eccubeConfig['eccube_save_image_dir'] . '/' . $filePdf;
        $fileUrl = $this->assetPackage->getUrl($filePdf, 'save_image');
        file_put_contents($filePath, $dompdf->output());
        return $fileUrl;
    }

load file font-family file .twig add style
ex for url: http://eccube.test/html/upload/qr/fonts/MeiryoJP-Bold.ttf (have to has http://)

         <style>
            @font-face {
                font-family: 'Meiryo UI';
src: url({{ eccube_config.app_url ~ '/html/upload/qr/fonts/MeiryoJP-Bold.ttf' }}) format("truetype");
font-weight: 1200;
                font-style: normal;
            }
            body {
                font-family: "Meiryo UI";
            }
        </style>

 or in header

<<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 <meta http-equiv="Content-type" content="application/xhtml+xml; charset=utf-8" /> 
<link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Tangerine&display=swap" rel="stylesheet" /> 
<style>
.m {
  font-family: 'Montserrat';
}
.t {
  font-family: 'Tangerine';
} 
</style> 

in body

<p class="m">xin chao helo メールの書式は正しくない</p>
<p class="t">xin chao helo メールの書式は正しくない</p>

Reference:
- https://github.com/dompdf/dompdf

* Explain option in Option of pdf

-  $isRemoteEnabled = true => can use font-face, and font cache can auto create when you create pdf

 /**
     * Enable remote file access
     *
     * If this setting is set to true, DOMPDF will access remote sites for
     * images and CSS files as required.
     *
     * ==== IMPORTANT ====
     * This can be a security risk, in particular in combination with isPhpEnabled and
     * allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...);
     * This allows anonymous users to download legally doubtful internet content which on
     * tracing back appears to being downloaded by your server, or allows malicious php code
     * in remote html pages to be executed by your server with your account privileges.
     *
     * This setting may increase the risk of system exploit. Do not change
     * this settings without understanding the consequences. Additional
     * documentation is available on the dompdf wiki at:
     * https://github.com/dompdf/dompdf/wiki
     *
     * @var bool
     */
    private $isRemoteEnabled = false;

- $isFontSubsettingEnabled" should set = true (default is true): isFontSubsettingEnabled is a configuration option that, when enabled, instructs the system to subset fonts, including only the characters used within the content, thereby reducing file size and improving performance.

Thank you

Tuesday, 5 March 2024

Validation by Using ValidatorInterface & Entities

 Example validate request data with 2 parameters:

- ids: array of integer

- qr_sizes: array of integer can null

1. Create ValidatorTrait

\app\Customize\Entity\Validator\ValidatorTrait.php

<?php

namespace Customize\Entity\Validator;

trait ValidatorTrait
{
    /**
     * setDataFromArray
     * @param array<string, string> $data Data for object.
     *
     * @return ValidatorTrait Object data.
     */
    public function setDataFromArray(array $data)
    {
        foreach ($data as $key => $value) {
            if (property_exists($this, $key)) {
                $this->$key = $value;
            }
        }
        return $this;
    }
}

2. Create DownloadQrCodeValidator

app\Customize\Entity\Validator\Product\DownloadQrCodeValidator.php

see mor Constrains here >> https://symfony.com/doc/5.x/validation.html

<?php
namespace Customize\Entity\Validator\Product;

use Symfony\Component\Validator\Constraints as Assert;
use Customize\Entity\Validator\ValidatorTrait;

class DownloadQrCodeValidator
{
    use ValidatorTrait;

    /**
     * ids
     *
     * @Assert\NotBlank(message="The ids must not be blank.")
     * @Assert\All({
     *     @Assert\Regex(pattern="/\d+/", message="All elements of the array ids must be number.")
     * })
     */
    public $ids;

    /**
     * qr_sizes
     *
     * @Assert\NotBlank(allowNull = true)
     * @Assert\All({
     *     @Assert\Regex(pattern="/\d+/", message="All elements of the array qr_sizes must be number.")
     * })
     */
    public $qr_sizes;
}

3. Using in DownloadQrCodeController

app\Customize\Entity\Validator\Product\DownloadQrCodeValidator.php

<?php

namespace Customize\Controller\Admin\Product;

use Customize\Common\ErrorsHelper;
use Customize\Entity\Validator\Product\DownloadQrCodeValidator;
use Customize\Service\Product\GetQrCodeService;
use Eccube\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Eccube\Repository\ProductRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class DownloadQrCodeController extends AbstractController
{
    /**
     * @var ProductRepository
     */
    protected $productRepository;

    /**
     * @var GetQrCodeService
     */
    protected $getQrCodeService;

    /**
     * @var ValidatorInterface
     */
    protected $validator;

    /**
     * DownloadQrCodeController constructor.
     *
     * @param ProductRepository $productRepository Description productRepository.
     * @param GetQrCodeService $getQrCodeService Description DesGetQrCodeService.
     * @param ValidatorInterface $validator Description ValidatorInterface.
     */
    public function __construct(
        ProductRepository $productRepository,
        GetQrCodeService $getQrCodeService,
        ValidatorInterface $validator
    ) {
        $this->productRepository = $productRepository;
        $this->getQrCodeService = $getQrCodeService;
        $this->validator = $validator;
    }

    /**
     * downloadQrCode
     * @param Request $request Request data.
     * @Route("/admin/download_qr", name="admin_download_qr", methods={"POST"})
     */
    public function downloadQrCode(Request $request)
    {
        if (!$request->isXmlHttpRequest()) {
            return $this->json(['status' => 'NG'], 400);
        }
        $this->isTokenValid();

        $data = (new DownloadQrCodeValidator())->setDataFromArray($request->request->all());
        $errors = $this->validator->validate($data);

        if ((count($errors) > 0)) {
            $this->addError(trans('admin.product.qr_code_form_download.download_failure', [
                '%errors%' => json_encode(ErrorsHelper::getMgsErrors($errors))
            ]), 'admin');
        } else {
            $this->addSuccess(trans('admin.product.qr_code_form_download.download_success'), 'admin');
            return new Response(
                $this->getQrCodeService->setData($data)->setHandler($this->getUser())->handle(),
                Response::HTTP_OK
            );
        }
    }
}

 Thank you

Publish npm package

  Để publish   pav-kit  lên NPM, bạn hãy làm theo các bước dưới đây. Tôi đã tạo thêm file  index.js  để đảm bảo gói tin hợp lệ. Bước 1: Tạo ...