Can't get consistant results

ookma-kyi

Aug 19th, 2024 02:31 PM

I am creating a factory for my new move model. I want to have it, so it can generate x types of moves. In my case I have the seeder generate 3 offensive moves and 3 defensive moves:

MoveSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Move;
use Database\Factories\MoveFactory;
use Illuminate\Database\Seeder;

class MoveSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // reset the counter
        MoveFactory::resetInstance();

        // Create offensive moves
        Move::factory()->count(3)->offensive()->create();

        // reset the counter
        MoveFactory::resetInstance();

        // Create corresponding defensive move
        Move::factory()->count(3)->defensive()->create();
    }
}

Here is my factory:

<?php

namespace Database\Factories;

use App\Enumerations\MoveType;
use App\Models\Move;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Log;

/**
 * @extends Factory<Move>
 */
class MoveFactory extends Factory
{
    // static variable to keep track if this is the 1st rank instance seeded
    protected static bool $firstInstance = true;

    // Static variables to keep track of move order for each type
    protected static int $orderCounter = 0;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        // Random offensive move array
        $randomOffensiveMoves = ['elbow strike', 'hook', 'jab', 'overhand punch', 'palm strike', 'spear hand', 'uppercut'];

        // Random defensive move array
        $randomDefensiveMoves = ['cross block', 'inside block', 'knife hand block', 'leg block', 'outside block', 'palm block'];

        // Determine the move type
        $type = $this->faker->randomElement([MoveType::OFFENSIVE, MoveType::DEFENSIVE]);

        // if this is the first rank instance
        if(self::$firstInstance) {

            Log::info("First Instance = True");
            // and set firstInstance to false for the next instance
            self::$firstInstance = false;
            Log::info("First Instance = Flipped False");
        } else {
            self::$orderCounter++;
            Log::info("First Instance = False");
        }

        // Set the name based on the move type
        if ($type == MoveType::OFFENSIVE) {
            $name = $this->faker->randomElement($randomOffensiveMoves);
            Log::info("Type = Offensive");
        } else {
            $name = $this->faker->randomElement($randomDefensiveMoves);
            Log::info("Type = Defensive");
        }
        Log::info("Name = $name");

        return [
            'name' => $name,
            'order' => self::$orderCounter,
            'move_type' => $type->value,
        ];
    }

    // Method to reset the instance
    public static function resetInstance(): void
    {
        self::$firstInstance = true;
        self::$orderCounter = 0;
    }

    public function offensive(): MoveFactory|Factory
    {
        return $this->state([
            'move_type' => MoveType::OFFENSIVE->value,
        ]);
    }

    public function defensive(): MoveFactory|Factory
    {
        return $this->state([
            'move_type' => MoveType::DEFENSIVE->value,
        ]);
    }
}

Here is my MoveType enum:

<?php

namespace App\Enumerations;

// enumerations for the move type
enum MoveType: int
{
    /** the move is an offensive one */
    case OFFENSIVE = 0;

    /** the move is an defensive one */
    case DEFENSIVE = 1;
}

Here is my move migration:

<?php

use App\Enumerations\MoveType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('moves', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->unsignedTinyInteger('order');
            $table->unsignedTinyInteger('move_type')->default(MoveType::OFFENSIVE);
            $table->timestamps();
        });

        // get the move types constants
        $move_type_offensive = MoveType::OFFENSIVE->value;
        $move_type_defensive = MoveType::DEFENSIVE->value;

        // Add the CHECK constraint
        DB::statement("ALTER TABLE moves ADD CONSTRAINT moves_is_valid_type CHECK (move_type IN ($move_type_offensive, $move_type_defensive))");
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('moves');
    }
};

The issue is what I am logging for debugging is different then what the database is showing me:

Log:

[2024-08-19 21:20:04] local.INFO: First Instance = True  
[2024-08-19 21:20:04] local.INFO: First Instance = Flipped False  
[2024-08-19 21:20:04] local.INFO: Type = Defensive  
[2024-08-19 21:20:04] local.INFO: Name = knife hand block  
[2024-08-19 21:20:04] local.INFO: First Instance = False  
[2024-08-19 21:20:04] local.INFO: Type = Offensive  
[2024-08-19 21:20:04] local.INFO: Name = palm strike  
[2024-08-19 21:20:04] local.INFO: First Instance = False  
[2024-08-19 21:20:04] local.INFO: Type = Offensive  
[2024-08-19 21:20:04] local.INFO: Name = spear hand  
[2024-08-19 21:20:04] local.INFO: First Instance = True  
[2024-08-19 21:20:04] local.INFO: First Instance = Flipped False  
[2024-08-19 21:20:04] local.INFO: Type = Offensive  
[2024-08-19 21:20:04] local.INFO: Name = jab  
[2024-08-19 21:20:04] local.INFO: First Instance = False  
[2024-08-19 21:20:04] local.INFO: Type = Offensive  
[2024-08-19 21:20:04] local.INFO: Name = palm strike  
[2024-08-19 21:20:04] local.INFO: First Instance = False  
[2024-08-19 21:20:04] local.INFO: Type = Offensive  
[2024-08-19 21:20:04] local.INFO: Name = overhand punch  

And what the database shows:

|_. id |_. name |_. order |_. move_type |_. created_at |_. updated_at |
| 1 | knife hand block | 0 | 0 | 2024-08-19 21:20:04 | 2024-08-19 21:20:04 |
| 2 | palm strike | 1 | 0 | 2024-08-19 21:20:04 | 2024-08-19 21:20:04 |
| 3 | spear hand | 2 | 0 | 2024-08-19 21:20:04 | 2024-08-19 21:20:04 |
| 4 | jab | 0 | 1 | 2024-08-19 21:20:04 | 2024-08-19 21:20:04 |
| 5 | palm strike | 1 | 1 | 2024-08-19 21:20:04 | 2024-08-19 21:20:04 |
| 6 | overhand punch | 2 | 1 | 2024-08-19 21:20:04 | 2024-08-19 21:20:04 |

Note how the move_type field is incorrect, knife hand block is defensive(1), palm strike is offensive(0) which matches the debug log but, not what's in the database. Thanks for your time.

bobbyiliev

Aug 20th, 2024 01:23 AM

Hey!

As far as I can tell, the main issue here is that the definition() method in your MoveFactory is randomly determining the move type, which is then being overridden by the offensive() and defensive() methods called in the seeder. This leads to inconsistencies between what's logged and what's actually saved to the database.

Here's how you might be able to fix this:

  1. Remove the random type selection from the definition() method.
  2. Make the offensive() and defensive() methods set both the type and the name.
  3. Use a separate counter for offensive and defensive moves.

Start by first updating your MoveFactory:

<?php

namespace Database\Factories;

use App\Enumerations\MoveType;
use App\Models\Move;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Log;

/**
 * @extends Factory<Move>
 */
class MoveFactory extends Factory
{
    protected static int $offensiveCounter = 0;
    protected static int $defensiveCounter = 0;

    // Random offensive move array
    protected static array $randomOffensiveMoves = ['elbow strike', 'hook', 'jab', 'overhand punch', 'palm strike', 'spear hand', 'uppercut'];

    // Random defensive move array
    protected static array $randomDefensiveMoves = ['cross block', 'inside block', 'knife hand block', 'leg block', 'outside block', 'palm block'];

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => '',  // Will be set in offensive() or defensive()
            'order' => 0,  // Will be set in offensive() or defensive()
            'move_type' => MoveType::OFFENSIVE->value,  // Default, will be overridden if necessary
        ];
    }

    public static function resetInstance(): void
    {
        self::$offensiveCounter = 0;
        self::$defensiveCounter = 0;
    }

    public function offensive(): MoveFactory|Factory
    {
        return $this->state(function (array $attributes) {
            $name = $this->faker->randomElement(self::$randomOffensiveMoves);
            Log::info("Creating Offensive Move: $name, Order: " . self::$offensiveCounter);
            return [
                'name' => $name,
                'order' => self::$offensiveCounter++,
                'move_type' => MoveType::OFFENSIVE->value,
            ];
        });
    }

    public function defensive(): MoveFactory|Factory
    {
        return $this->state(function (array $attributes) {
            $name = $this->faker->randomElement(self::$randomDefensiveMoves);
            Log::info("Creating Defensive Move: $name, Order: " . self::$defensiveCounter);
            return [
                'name' => $name,
                'order' => self::$defensiveCounter++,
                'move_type' => MoveType::DEFENSIVE->value,
            ];
        });
    }
}

Key changes:

  1. Removed the random type selection from definition().
  2. Created separate counters for offensive and defensive moves.
  3. Updated offensive() and defensive() methods to set the name, order, and type.
  4. Added more detailed logging.

After that update your MoveSeeder so it's using these new methods correctly:

<?php

namespace Database\Seeders;

use App\Models\Move;
use Database\Factories\MoveFactory;
use Illuminate\Database\Seeder;

class MoveSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // reset the counter
        MoveFactory::resetInstance();

        // Create offensive moves
        Move::factory()->count(3)->offensive()->create();

        // Create defensive moves
        Move::factory()->count(3)->defensive()->create();
    }
}

With these changes, you should now see consistent results between your logs and the database. The offensive moves will have move_type = 0 and the defensive moves will have move_type = 1, and the order will be correct for each type.

To test this, you can run your seeder and check both the logs and the database. The logs should now show something like:

Creating Offensive Move: jab, Order: 0
Creating Offensive Move: palm strike, Order: 1
Creating Offensive Move: uppercut, Order: 2
Creating Defensive Move: cross block, Order: 0
Creating Defensive Move: inside block, Order: 1
Creating Defensive Move: palm block, Order: 2

And your database should reflect these exact moves and orders.

If you're still experiencing issues after making these changes, please let me know, and we can further debug the problem.

- Bobby