Search by product attributes WooCommerce/WordPress

• I have a product.
• In the product card there are 2 attributes (with different id’s), with two different prices.
• In wp_postmeta the product has 2(!!!) meta_key == _price.

Accordingly, the search works on the 1st price.
How to make the search work on the 2nd price?

Attribute id of the 2nd price is ‘rentfortheevent’ (‘pa_rentfortheevent’ in the attributes table in database).

Right now the search by price is done like this:

function set_price($q)
  {
   $min_price = $_GET["min_price"];
   $max_price = $_GET["max_price"];

   $meta_query = $q -> get("meta_query");

   if ($min_price)
    {
     $meta_query[] = 
      [
       "key"     => "_price",
       "value"   => $min_price,
       "type"    => "NUMERIC",
       "compare" => ">=",
      ];
    }

   if ($max_price)
    {
     $meta_query[] = 
      [
       "key"     => "_price",
       "value"   => $max_price,
       "type"    => "NUMERIC",
       "compare" => "<=",
      ];
    }

   $q -> set("meta_query", $meta_query);
  }

 add_action("woocommerce_product_query", "set_price");
}

wp_postdata screenshot with two _price

Getting Session start error message in debug log file on my WordPress site

I am getting these two session start errors on my site and I have done everything to fix but they persist.

[26-May-2025 03:30:02 UTC] PHP Warning: session_start(): Session cannot be started after headers have already been sent in /home/u402881756/domains/realrender3d.app/public_html/books/wp-content/plugins/custom-books/includes/form-handler.php on line 5
[26-May-2025 03:30:02 UTC] PHP Warning: session_start(): Session cannot be started after headers have already been sent in /home/u402881756/domains/realrender3d.app/public_html/books/wp-content/plugins/custom-books/book-personalizer.php on line 12

Here is my main plugin file:

    <?php
/**
 * Plugin Name: Custom Book Personalizer
 * Description: A plugin for personalizing children's books.
 * Version: 1.0
 * Author: Nastin Gwaza
 */
if (!defined('ABSPATH')) exit; // Exit if accessed directly

add_action('plugins_loaded', function () {
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }
}, 1); // Run very early

// ✅ Plugin constants
define('BP_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('BP_PLUGIN_URL', plugin_dir_url(__FILE__));

// ✅ Load includes
$includes = [
    BP_PLUGIN_DIR . 'includes/scripts.php',
    BP_PLUGIN_DIR . 'includes/admin-product-meta.php',
    BP_PLUGIN_DIR . 'includes/woocommerce-hooks.php',
    BP_PLUGIN_DIR . 'includes/settings.php',
    BP_PLUGIN_DIR . 'includes/face-swap-handler.php', // Added AI image generator
];

foreach ($includes as $file) {
    if (file_exists($file)) {
        require_once $file;
    }
}

// ✅ Preview page shortcode
add_shortcode('book_preview_page', function () {
    ob_start();
    include BP_PLUGIN_DIR . 'includes/shortcode-preview-page.php';
    return ob_get_clean();
});

// ✅ Personalization form shortcode with product_id handling
function bp_render_personalization_page($atts) {
    $atts = shortcode_atts([
        'product_id' => get_the_ID()
    ], $atts);

    $product_id = intval($atts['product_id']);

    ob_start();
    include BP_PLUGIN_DIR . 'includes/personalization-page.php';
    return ob_get_clean();
}

function bp_register_shortcodes_after_wc_loaded() {
    if (function_exists('wc_get_product')) {
        add_shortcode('book_personalizer', 'bp_render_personalization_page');
    }
}
add_action('init', 'bp_register_shortcodes_after_wc_loaded');

// ✅ Hook form submission
add_action('admin_post_bp_handle_form', 'bp_handle_form_submission');
add_action('admin_post_nopriv_bp_handle_form', 'bp_handle_form_submission');
add_action('admin_post_bp_add_to_cart', 'bp_add_to_cart_from_preview');
add_action('admin_post_nopriv_bp_add_to_cart', 'bp_add_to_cart_from_preview');



require_once BP_PLUGIN_DIR . 'includes/form-handler.php';


// 1. Add personalization data to cart item
add_filter('woocommerce_add_cart_item_data', function($cart_item_data, $product_id, $variation_id) {
    if (isset($_POST['personalization_data'])) {
        $cart_item_data['personalization_data'] = json_decode(stripslashes($_POST['personalization_data']), true);
        $cart_item_data['unique_key'] = md5(microtime().rand()); // Prevent merging similar items
    }
    return $cart_item_data;
}, 10, 3);

// 2. Show personalization on cart and checkout
add_filter('woocommerce_get_item_data', function($item_data, $cart_item) {
    if (isset($cart_item['personalization_data'])) {
        $pdata = $cart_item['personalization_data'];

        if (!empty($pdata['child_name'])) {
            $item_data[] = [
                'key'   => 'Child's Name',
                'value' => sanitize_text_field($pdata['child_name']),
            ];
        }

        if (!empty($pdata['age_group'])) {
            $item_data[] = [
                'key'   => 'Age Group',
                'value' => sanitize_text_field($pdata['age_group']),
            ];
        }

        if (!empty($pdata['gender'])) {
            $item_data[] = [
                'key'   => 'Gender',
                'value' => sanitize_text_field($pdata['gender']),
            ];
        }
    }

    return $item_data;
}, 10, 2);

// 3. Save personalization in order meta
add_action('woocommerce_add_order_item_meta', function($item_id, $values) {
    if (!empty($values['personalization_data'])) {
        wc_add_order_item_meta($item_id, 'Personalization Details', $values['personalization_data']);
    }
}, 10, 2);

and here is my form-handler.php file:

    <?php
defined('ABSPATH') || exit;

if (!session_id()) {
    session_start();
}

add_action('admin_post_nopriv_bp_handle_form_submission', 'bp_handle_form_submission');
add_action('admin_post_bp_handle_form_submission', 'bp_handle_form_submission');

function bp_handle_form_submission() {
    if (!isset($_POST['bp_personalize_nonce']) || !wp_verify_nonce($_POST['bp_personalize_nonce'], 'bp_personalize_action')) {
        $product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
        wp_redirect(home_url('/personalize-book?error=invalid_nonce&product_id=' . $product_id));
        exit;
    }

    $required_fields = ['product_id', 'child_name', 'age_group', 'gender', 'birth_month', 'relation'];
    foreach ($required_fields as $field) {
        if (empty($_POST[$field])) {
            $product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
            wp_redirect(home_url('/personalize-book?error=missing_fields&product_id=' . $product_id));
            exit;
        }
    }

    $product_id = intval($_POST['product_id']);
    $product = wc_get_product($product_id);

    if (!$product) {
        wp_redirect(home_url('/personalize-book?error=invalid_input&product_id=' . $product_id));
        exit;
    }

    $child_image_url = '';

    if (!empty($_FILES['child_image']['tmp_name'])) {
        require_once(ABSPATH . 'wp-admin/includes/file.php');
        $uploaded = media_handle_upload('child_image', 0);

        if (is_wp_error($uploaded)) {
            wp_redirect(home_url('/personalize-book?error=image_upload&product_id=' . $product_id));
            exit;
        } else {
            $child_image_url = wp_get_attachment_url($uploaded);
        }
    }

    $_SESSION['bp_personalization'] = [
        'product_id'      => $product_id,
        'child_name'      => sanitize_text_field($_POST['child_name']),
        'age_group'       => sanitize_text_field($_POST['age_group']),
        'gender'          => sanitize_text_field($_POST['gender']),
        'birth_month'     => sanitize_text_field($_POST['birth_month']),
        'relation'        => sanitize_text_field($_POST['relation']),
        'child_image_url' => esc_url_raw($child_image_url),
    ];

    // Redirect to face swap handler (or preview page)
    wp_safe_redirect(admin_url('admin-post.php?action=bp_handle_face_swap'));
    exit;
}

Did I leave any white space anywhere?

I required three levels of Admin in Laravel 11 [closed]

I required three levels of admin in laravel 11

  1. Supar Admin
  2. Office Admin
  3. Admin Staffs

The admin table is like below

| id | username    | password | hierarchy |
|----|-------------|----------|-----------|
| 1  | superadmin  | ******** | 1         |
| 2  | officeadmin | ******** | 2         |
| 3  | adminstaff  | ******** | 3         |

In AdminController -> login function I wrote:

public function login(Request $request){
    if($request->isMethod('post')){
        $data=$request->all();

        $rules = [
            'email'=> 'required|email|max:255',
            'password'=>'required',
        ];

        $customMessages = [
            'email.required'=> 'Email is Required',
            'email.email'=>'Valid Email is Required',
            'password.required'=>'Password is Required',
        ];

        $this->validate($request,$rules,$customMessages);
        
        if(Auth::guard('admin')->attempt(['email'=>$data['email'],'password'=>$data['password']])){
            return redirect('admin/dashboard');
        }
        else{
            Session::flash('error_message','Invalid Email or Password!');
            return redirect()->back();
        }
    }
    return view('admin.admin_login');
}

Now after successful login, I wish that

  • For hierarchy 1, url goes to ‘admin/dashboard’
  • For hierarchy 2, url goes to ‘office/dashboard’
  • For hierarchy 3, url goes to ‘staff/dashboard’

Please guide me how to do that.

Thanks & Regards,

$_SERVER[‘SCRIPT_NAME’] and $_SERVER[‘PHP_SELF’] returns index.php although the script is run on other page(s) [duplicate]

I am building an HTML form with PHP validation inside WordPress. Initially I built it on index.php and had no issues. But upon moving the form to another page (/form.php) the problem arise, since I want the form to be run and error handled on the same page (equivalent to action="").

I have tried with $_SERVER['PHP_SELF']:

<form method="POST" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']);?>">
    <input name="test">
    <button type="submit" name="submit">Send</button>
</form>

with $_SERVER['SCRIPT_NAME']:

<form method="POST" action="<?php echo htmlspecialchars($_SERVER['SCRIPT_NAME']); ?>">
    <input name="test">
    <button type="submit" name="submit">Send</button>
</form>

and with $_SERVER['SCRIPT_FILENAME']:

<form method="POST" action="<?php echo htmlspecialchars($_SERVER['SCRIPT_FILENAME']); ?>">
    <input name="test">
    <button type="submit" name="submit">Send</button>
</form>

but the form directs me to index.php although form.php (or the like) is excepted.

When checking the output of var_dump($_SERVER) on the page, these values are outputed on the screen:

["PHP_SELF"]=> string(24) "/root-folder-name/index.php"
["SCRIPT_NAME"]=> string(24) "/root-folder-name/index.php"
["SCRIPT_FILENAME"]=> string(40) "[...]/root-folder-name/index.php"

What am I missing?


This one returns the path of the current document:
["REQUEST_URI"]=> string(20) "/root-folder-name/form/".

How to link nginx and php-fpm with docker compose?

I’m learning docker compose and trying to run the project provided from some Docker course.
There is the project to link nginx:latest and php:8.2-fpm containers to display message from index.php. But I cannot reproduce the results. It’s supposed to display “Hello from PHP!” message, when I go to localhost, but I’ve got this message instead:


    Welcome to nginx!
    If you see this page, the nginx web server is successfully installed and working.
    Further configuration is required.
    
    For online documentation and support please refer to nginx.org.
    Commercial support is available at nginx.com.
    
    Thank you for using nginx.

As I understand nginx container is ok, and the problem is with linking nginx and php.

Explain how to solve this issue, please.

Project structure:

    project/
    ├── docker-compose.yml
    ├── nginx/
    │   ├── default.conf
    │   ├── html/
    │   └── Dockerfile
    ├── php/
    │   ├── index.php
    │   └── Dockerfile

Here is docker-compose.yml:


    version: '3.8'
    services:
      nginx:
        build: ./nginx
        image: nginx:latest
        container_name: nginx
        ports:
          - "80:80"
        volumes:
          - ./nginx/html:/var/www/html
        depends_on:
          - php
        networks:
          - app_network
    
      php:
        build: ./php
        image: php:8.2-fpm
        container_name: php
        volumes:
          - ./php:/var/www/html
        networks:
          - app_network
    
    networks:
      app_network:
        driver: bridge

nginx/default.conf:


    server {
        listen 80;
        server_name localhost;
    
        root /var/www/html;
        index index.php index.html;
    
        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }
    
        location ~ .php$ {
            fastcgi_pass php:9000;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
    }

nginx/Dockerfile:


    FROM nginx:latest
    COPY default.conf /etc/nginx/conf.d/
    COPY ./html /usr/share/nginx/html

Docker Desktop logs:

  • php:

    NOTICE: fpm is running, pid 1
    NOTICE: ready to handle connections

  • nginx:

    /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
    /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
    /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
    10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
    10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
    /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
    /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
    /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
    /docker-entrypoint.sh: Configuration complete; ready for start up
    2025/05/25 08:03:14 [notice] 1#1: using the "epoll" event method
    2025/05/25 08:03:14 [notice] 1#1: nginx/1.27.5
    2025/05/25 08:03:14 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14) 
    2025/05/25 08:03:14 [notice] 1#1: OS: Linux 5.15.167.4-microsoft-standard-WSL2
    2025/05/25 08:03:14 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
    2025/05/25 08:03:14 [notice] 1#1: start worker processes
    2025/05/25 08:03:14 [notice] 1#1: start worker process 29
    2025/05/25 08:03:14 [notice] 1#1: start worker process 30
    2025/05/25 08:03:14 [notice] 1#1: start worker process 31
    2025/05/25 08:03:14 [notice] 1#1: start worker process 32
    2025/05/25 08:03:14 [notice] 1#1: start worker process 33
    2025/05/25 08:03:14 [notice] 1#1: start worker process 34
    2025/05/25 08:03:14 [notice] 1#1: start worker process 35
    2025/05/25 08:03:14 [notice] 1#1: start worker process 36
    2025/05/25 08:03:14 [notice] 1#1: start worker process 37

Sorry about such amount of text.

Mariadb time data corrupted after server crash

I have a problem when the server is down and when it is finished processing, the old data in the database is time-shifted.

column information: created_at - timestamp

i use Laravel 11 + mariadb, php8.4,

in Laravel i have set Timezone of our country

i am very sure about the time because before that i still check database daily and keep the record data with the time of the timezone i set

For example, the record is at 12 noon but it is recorded as 18:00 pm.

i tried to set timezone in mariadb and reformat with laravel for timezone +7 but the time is completely wrong

===
update: i double checked my old data before down is not affected, but the new records after server up are wrong

Server side timer to execute PHP code upon expiration? [closed]

I’m looking for a server side timer that would execute a PHP file at expiration. The timer should be controlled by another PHP file to start, stop and reset the timer.

The PHP module EvTimer would work great in this situation, but it is not available on the Bluehost shared servers I’m using.
Cron jobs? Hmmm, could those be activated when the command is received, canceled when successful?
Other options?

Need to have the custom Login / Signup Authentication for non-ecommerce wordpress website [closed]

I’m trying building a custom plugin in WordPress that requires login functionality. Could anyone please share their knowledge or guidance on the best way to handle login authentication?

I’m not in a mood of considering WooCommerce for its login features, I actually don’t need the full suite of eCommerce features like orders, products, etc., since this is a non-eCommerce website.

Are there any lightweight plugins that provide just login and authentication? Or would it be better to build a custom login/signup system that stores registered users in a separate table?

Any advice would be appreciated. Thanks!

How to resolve barcode scanner promblem in web? [closed]

I have a problem with my web source code. I have made sure that the site is accessed via https and camera access permission is granted. However, the barcode scan display does not appear and only displays a white blank. I tried to access it via chrome on android.

This is my code

    <?php
$satuan_list = ['pcs', 'dus', 'pack', 'ball', 'renteng'];
?>
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <title>Form Input Barang</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    video {
      width: 100%;
      height: auto;
    }
  </style>
</head>
<body class="bg-light py-4">
<div class="container">
  <h2 class="text-center mb-4">Form Input Barang</h2>

  <form action="save_barang.php" method="POST" enctype="multipart/form-data" class="bg-white p-4 rounded shadow">
    <div class="mb-3">
      <label class="form-label">Nama Barang</label>
      <input type="text" name="nama_barang" class="form-control" required>
    </div>

    <div class="mb-3">
      <label class="form-label">Kode Gudang</label>
      <input type="text" name="kode_gudang" class="form-control" required>
    </div>

    <div class="mb-3">
      <label class="form-label">Vendor Penyedia</label>
      <input type="text" name="vendor" class="form-control" required>
    </div>

    <div class="mb-4">
      <label class="form-label">Foto Barang</label>
      <input type="file" name="foto" class="form-control" accept="image/*" capture="environment">
    </div>

    <?php foreach ($satuan_list as $satuan): ?>
    <div class="border rounded p-3 mb-4">
      <h5 class="mb-3">Satuan: <?= ucfirst($satuan) ?></h5>
      <input type="hidden" name="satuan[]" value="<?= $satuan ?>">

      <div class="row g-3">
        <div class="col-md-4">
          <label class="form-label">Stok (<?= $satuan ?>)</label>
          <input type="number" name="stok_<?= $satuan ?>" class="form-control">
        </div>

        <div class="col-md-4">
          <label class="form-label d-flex justify-content-between">
            <span>Barcode (<?= $satuan ?>)</span>
            <button type="button" class="btn btn-sm btn-outline-primary" onclick="startScanner('barcode_<?= $satuan ?>')">Scan</button>
          </label>
          <input type="text" name="barcode_<?= $satuan ?>" id="barcode_<?= $satuan ?>" class="form-control barcode-input">
        </div>

        <div class="col-md-4">
          <label class="form-label">Harga Eceran</label>
          <div class="input-group">
            <span class="input-group-text">Rp.</span>
            <input type="number" step="0.01" name="harga_eceran_<?= $satuan ?>" class="form-control">
          </div>
        </div>

        <div class="col-md-4">
          <label class="form-label">Harga Grosir</label>
          <div class="input-group">
            <span class="input-group-text">Rp.</span>
            <input type="number" step="0.01" name="harga_grosir_<?= $satuan ?>" class="form-control">
          </div>
        </div>

        <div class="col-md-4">
          <label class="form-label">Min. Pembelian Harga Grosir</label>
          <input type="number" name="min_grosir_<?= $satuan ?>" class="form-control">
        </div>

        <?php if ($satuan != 'pcs'): ?>
        <div class="col-md-4">
          <label class="form-label">Isi per <?= $satuan ?> (pcs)</label>
          <input type="number" name="isi_per_pcs_<?= $satuan ?>" class="form-control">
        </div>
        <?php endif; ?>
      </div>
    </div>
    <?php endforeach; ?>

    <div class="d-grid gap-2">
      <button type="submit" class="btn btn-primary">Simpan Barang</button>
      <a href="list_barang.php" class="btn btn-secondary">Lihat Daftar Barang</a>
    </div>
  </form>

  <!-- Modal Scanner -->
  <div class="modal fade" id="scannerModal" tabindex="-1" aria-labelledby="scannerModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">Scan Barcode</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Tutup" onclick="stopScanner()"></button>
        </div>
        <div class="modal-body">
          <video id="preview" autoplay muted playsinline style="width: 100%; border: 1px solid #ccc; border-radius: .5rem;"></video>
        </div>
        <div class="modal-footer">
          <button class="btn btn-secondary" data-bs-dismiss="modal" onclick="stopScanner()">Tutup</button>
        </div>
      </div>
    </div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/@ericblade/[email protected]/dist/quagga.min.js"></script>

<script>
let activeInput = null;
const scannerModal = new bootstrap.Modal(document.getElementById('scannerModal'));

function startScanner(inputId) {
  activeInput = document.getElementById(inputId);
  scannerModal.show();

  if (Quagga.running) {
    Quagga.stop();
  }

  console.log("Mulai inisialisasi scanner...");

  Quagga.init({
    inputStream: {
      type: "LiveStream",
      constraints: {
        facingMode: "environment"
      },
      target: document.querySelector('#preview')
    },
    decoder: {
      readers: ["ean_reader", "code_128_reader", "upc_reader"]
    }
  }, function(err) {
    if (err) {
      console.error("Gagal inisialisasi Quagga:", err);
      alert("Tidak bisa akses kamera: " + err.message);
      return;
    }
    console.log("Scanner berhasil dijalankan!");
    Quagga.start();
  });
}

function stopScanner() {
  if (Quagga.running) {
    Quagga.stop();
  }
}

Quagga.onDetected(result => {
  if (!result || !result.codeResult || !result.codeResult.code) return;

  const code = result.codeResult.code;
  if (activeInput) {
    activeInput.value = code;
    stopScanner();
    scannerModal.hide();
  }
});
</script>
</body>
</html>

and I also attached the display via the Android Chrome webbarcode visual

Problem with Inertia.js SSR not rendering server-side in Laravel 12 with React

I’m experiencing issues with Server-Side Rendering (SSR) in a fresh Laravel 12 installation using the React starter kit. Despite following the documentation, the page continues to render client-side (CSR) instead of server-side.

Setup:

  • Laravel 12 (fresh installation)
  • Inertia.js with React starter kit
  • Node.js 22
  • Running composer dev:ssr (Vite SSR server starts on port 13714)

Problem:

  • SSR build completes without errors (resources/js/ssr.tsx)
  • Inertia SSR server runs on port 13714
  • Browser still shows CSR behavior (no server-rendered markup)

Same issue occurs in production with:

  • PM2 running php artisan inertia:start-ssr (tried also “node bootstrap/ssr/ssr.js”)

Server should return fully rendered HTML on initial page load.

How can I debug why SSR isn’t taking effect? Are there additional configuration steps I might be missing for Laravel 12?

Why am I getting unauthorized response error from Google OAuth?

I made a function which tries to get access token from refresh token, but i get an error:

Error: Failed to get access token, HTTP code: 401, response: { "error": "unauthorized_client", "error_description": "Unauthorized" }

I tried to test the url with keys on https://reqbin.com/ but there i also get unauthorized.

I tested some code with access token and it works so refresh token should also work.

function get_google_access_token() {
    $client_id = 'x';
    $client_secret = 'x';
    $refresh_token = 'x';

    $url = "https://oauth2.googleapis.com/token";

    $data = [
        "grant_type" => "refresh_token",
        "client_id" => $client_id,
        "client_secret" => $client_secret,
        "refresh_token" => $refresh_token,
    ];

    $options = [
        CURLOPT_URL => $url,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($data),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/x-www-form-urlencoded"
        ],
    ];

    $curl = curl_init();
    curl_setopt_array($curl, $options);
    $response = curl_exec($curl);
    $httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    if ($httpcode !== 200) {
        throw new Exception("Failed to get access token, HTTP code: $httpcode, response: $response");
    }

    $result = json_decode($response, true);
    if (!isset($result['access_token'])) {
        throw new Exception("No access token found in response");
    }

    return $result['access_token'];
}

try {
    $accessToken = get_google_access_token();
    echo "Access Token: " . $accessToken;
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}



Reading `STDIN` on Windows in MSYS2

Setup

  1. Symfony Console application.
  2. Packed into a Phar with Box.
  3. Distributed as an executable for Win, Linux, and MacOS with PHPMicro.

Execution

  1. MSYS2 on Win11.
  2. make import calls app import < "$filename" in a loop for an ordered list of inputs.
    1. Alternatively, cat "$filename" | app import.

Problem

Cannot read from STDIN: ftell(STDIN) returns false. There’s no data in the stream. Similar story with fopen('php://stdin', 'r').

winpty

Without winpty, I see my debug output, and can e.g. inspect the stream (that’s how I know it’s empty).

With winpty, none of that happens, and instead I get:

$ winpty un1q import:generic < datafile.csv
stdin is not a tty

Remarks

Works fine on Linux. I don’t really understand how MSYS2 works, especially with streams and pipes. From what I understood, it should transform my Bash syntax into something it runs in CMD or PowerShell (none of which I’m very familiar with).

Phinx targeted rollback gives No migrations to rollback

I created the following migration:

final class UpdateApiClientType extends AbstractMigration
{
    public function up(): void
    {
        $this->execute("UPDATE Api_Clients SET Type = 'ibe' WHERE ClientName = 'Test'");
    }

    function down(): void
    {
        $this->execute("UPDATE Api_Clients SET Type = 'ota' WHERE ClientName = 'Test'");
    }
}

The phinx migrate executed well. My phinxlog table contains:

20250523065423|UpdateApiClientType|2025-05-23 09:03:18|2025-05-23 09:03:18|0

However when I execute phinx rollback -t 20250523065423 I get:

No migrations to rollback

Any ideas?

MySQL 8 choosing sub-optimal indexing on large table despite suitable indexing

I’m working on a Laravel 12 project running MySQL 8.4. I have various models like Buyer, BuyerTier, Application and PingtreeGroup and I want to store raw transactional models in my PingtreeTransaction model.

This table will store around 500,000 entries a day, it’s schema, minus indexing looks like:

Schema::create('pingtree_transactions', function (Blueprint $table) {
    $table->ulid('id');
    $table->foreignId('company_id');
    $table->foreignId('application_id');
    $table->foreignId('buyer_id');
    $table->foreignId('buyer_tier_id');
    $table->foreignId('pingtree_group_id');
    $table->foreignId('pingtree_id');
    $table->mediumInteger('processing_duration')->default(0);
    $table->smallInteger('request_response_code')->default(200);
    $table->decimal('commission', 8, 2)->default(0.00);
    $table->string('result', 32)->default('unknown');
    $table->string('request_url')->nullable();
    $table->integer('partition_id');
    $table->date('processed_on');
    $table->dateTime('processing_started_at');
    $table->dateTime('processing_ended_at')->nullable();
    $table->timestamps();

    $table->primary(['id', 'partition_id']);
});

The various query use cases are as follows for joining transactions to models are:

  1. Fetch all pingtree transactions for any given application
  2. Fetch all pingtree transactions for any given application between two dates
  3. Fetch all pingtree transactions for any given buyer
  4. Fetch all pingtree transactions for any given buyer between two dates
  5. etc…

But then, there’s a front-end page that’s paginated and shows a date/time picker along with a tags component for each model allowing a user to filter all transactions, for example:

enter image description here

  1. Show me all pingtree transactions for the past 3 days where the Buyer is either “foo” or “Bar”, and where the BuyerTier is “a” and “b” where the result is either “accepted” or “declined” on any of them.

A user might not always include all fields in their search for models, they might only want to see everything over a period minus specific models.

For the end user, there’s a lot of possible combinations for reporting via this front-end page since this is a business choice.

So in summary, there’s two cases:

  1. Individual model joining
  2. A report page with various filters

Indexing dilemas…

Since I want to join individual models which won’t require a date, like the foreignId columns, I would’ve thought adding the following indexes are suitable:

$table->index(['application_id']);
$table->index(['processed_on']);
$table->index(['company_id']);
$table->index(['application_id']);
$table->index(['buyer_id']);
$table->index(['buyer_tier_id']);
$table->index(['result']);
$table->index(['partition_id']);
$table->index(['processed_on']);
$table->index(['processing_started_at']);
$table->index(['processing_ended_at']);

On a table with millions of rows, adding new indexes is going to lock the table, but, the issue above, is now because I don’t have a composite index, and the dates are ranges, the cardinality is really high on those columns, and lower on the buyer and buyer tier columns, so the database ends up weirdly just picking one index for processing_started_at which ends up taking minutes to load.

explain select
  *
from
  `pingtree_transactions`
where
  `company_id` in (2, 1)
  and `buyer_id` in ("154", "172")
  and `buyer_tier_id` in ("652")
  and `processing_started_at` >= '2025-05-21 23:00:00'
  and `processing_ended_at` <= '2025-05-23 22:59:59'
  and `result` in ("accepted")
order by
  `processing_started_at` desc
limit
  26 offset 0

If I then add some composite index with multiple columns in there like:

$table->index([
    'company_id',
    'buyer_tier_id',
    'buyer_id',
    'result',
    'processing_started_at',
    'processing_ended_at'
], 'composite_pingtree_transactions_all_index');

Then it only appears to use it if all of the columns are in the search query and is incredibly fast at around 5ms, but given the various combinations in filtering, this would then seemingly bloat the database with all the combinations, and if one field is missed out, it ends up falling back to a sub-optimal index.

Essentially, what combination of indexes then would best to always utilise indexing?

The reason for adding:

$table->primary(['id', 'partition_id']);

Is because I’m experimenting with partitioning, and partition_id would house the current day in YYYYMMDD format, so there would be a partition for each day, but when trying this, and adding partition id into the query it seems to use partition pruning but no indexing.

So the question here is, what indexing should I add for my use cases defined above.