jmacdiarmid's avatar

Laravel 10 - Model::save not working

I'm building a book library management system. In my BookController, when doing $book->save() or $book->saveorFail() it returns successful but no records are written to the database. Also, I'm not receiving any errors or notifications.

Here's my code:

In migration:

        Schema::create('books', static function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->longText('content');
            $table->decimal('price');
            $table->string('year_published', 4);
            $table->string('cover')->nullable();
            $table->integer('author_id');
            $table->timestamps();
        });

in Model:

class Book extends Model
{
    use HasFactory;

    protected $primaryKey = 'id';

    protected $fillable = [
        'title',
        'content',
        'price',
        'year_published',
        'cover',
        'author_id',
    ];
}

In my controller

    public function store(Request $request)
    {
//        try
//        {
            $validator = Validator::make($request->all(), [
                'title' => 'required|unique:books|max:255',
//                'cover' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
                'content' => 'required|max:1000',
                'price' => 'required',
                'year_published' => 'required',
            ]);

            if ($validator->fails()) {
                return redirect()->back()->withErrors($validator)->withInput();
            }

            $image = null;
            $coverName = '';
            if ($request->file('cover'))
            {
                $image = $request->file('cover');
                $coverName = time().'.'.$image->getClientOriginalExtension();
            }

            $book = new Book();
            $book->title = $request['title'];
            $book->content = $request['content'];
            $book->price = $request['price'];
            $book->year_published = $request['year_published'];
//            $book->cover = $coverName;
            $book->author_id = random_int(1, 10);

            if ($book->saveOrFail()) {
                if ($request->hasFile('cover')) {
                    $destinationPath = public_path('/assets/books/' . $book->id . '/');

                    // If directory is not there, create it.
                    if (!is_dir($destinationPath))
                    {
                        if (!mkdir((string)$destinationPath) && !is_dir((string)$destinationPath)) {
                            throw new \RuntimeException(sprintf('An error occurred when creating directory "%s".', (string)$destinationPath));
                        }
                        chmod((string)$destinationPath, 0755);
                    }

                    // If the directory has images , clean up first before upload.
                    if (!empty(scandir($destinationPath)))
                    {
                        File::cleanDirectory($destinationPath);
                    }

                    // directory is empty, upload image, update image name in database.
                    $image->move($destinationPath, $coverName);
                }

                return redirect()->back()->with('success', 'Book added successfully');
            }

//        } catch(\Exception $ex) {
//            Log::error($ex->getMessage());
//        }
    }

Any thoughts about what I'm not doing right and/or how to fix?

0 likes
16 replies
jaseofspades88's avatar

Above your $book = new Book(); line, what does dd($request->all()); return?

jmacdiarmid's avatar

@jaseofspades88 That returns:

Configuration: D:\laragon\www\library-using-tdd\phpunit.xml

Illuminate\Http\Request^ {#3225 // app\Http\Controllers\BookController.php:58
  +attributes: Symfony\Component\HttpFoundation\ParameterBag^ {#3227
    #parameters: []
  }
  +request: Symfony\Component\HttpFoundation\InputBag^ {#3226
    #parameters: array:4 [
      "title" => "Quia at saepe illo dolorum dolorem officia nemo."
      "content" => "Dolores modi dignissimos tempora optio neque dolor qui. Quaerat quas explicabo dolorem totam. Saepe est nisi dolore fuga provident exercitationem libero eaque."
      "price" => 4103
      "year_published" => 1755
    ]
  }
  +query: Symfony\Component\HttpFoundation\InputBag^ {#3233
    #parameters: []
  }
  +server: Symfony\Component\HttpFoundation\ServerBag^ {#3229
    #parameters: array:18 [
      "SERVER_NAME" => "library-using-tdd.test"
      "SERVER_PORT" => 80
      "HTTP_HOST" => "library-using-tdd.test"
      "HTTP_USER_AGENT" => "Symfony"
      "HTTP_ACCEPT" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
      "HTTP_ACCEPT_LANGUAGE" => "en-us,en;q=0.5"
      "HTTP_ACCEPT_CHARSET" => "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
      "REMOTE_ADDR" => "127.0.0.1"
      "SCRIPT_NAME" => ""
      "SCRIPT_FILENAME" => ""
      "SERVER_PROTOCOL" => "HTTP/1.1"
      "REQUEST_TIME" => 1689606354
      "REQUEST_TIME_FLOAT" => 1689606354.5455
      "PATH_INFO" => ""
      "REQUEST_METHOD" => "POST"
      "CONTENT_TYPE" => "application/x-www-form-urlencoded"
      "REQUEST_URI" => "/books/store"
      "QUERY_STRING" => ""
    ]
  }
  +files: Symfony\Component\HttpFoundation\FileBag^ {#3230
    #parameters: []
  }
  +cookies: Symfony\Component\HttpFoundation\InputBag^ {#3228
    #parameters: []
  }
  +headers: Symfony\Component\HttpFoundation\HeaderBag^ {#3231
    #headers: array:6 [
      "host" => array:1 [
        0 => "library-using-tdd.test"
      ]
      "user-agent" => array:1 [
        0 => "Symfony"
      ]
      "accept" => array:1 [
        0 => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
      ]
      "accept-language" => array:1 [
        0 => "en-us,en;q=0.5"
      ]
      "accept-charset" => array:1 [
        0 => "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
      ]
      "content-type" => array:1 [
        0 => "application/x-www-form-urlencoded"
      ]
    ]
    #cacheControl: []
  }
  #content: null
  #languages: null
  #charsets: null
  #encodings: null
  #acceptableContentTypes: null
  #pathInfo: "/books/store"
  #requestUri: "/books/store"
  #baseUrl: ""
  #basePath: null
  #method: "POST"
  #format: null
  #session: Illuminate\Session\Store^ {#2247
    #id: "GFNv63MFzalOEiXMbG8aAyOrZdU8yWiBnzGkklOX"
    #name: "library_project_using_tdd_session"
    #attributes: & array:1 [
      "_token" => "FRKQfqhsd5MSXX3N34ChkxClB57KDyPPPtbz1W6K"
    ]
    #handler: Illuminate\Session\ArraySessionHandler^ {#2256
      #storage: []
      #minutes: "120"
    }
    #serialization: "php"
    #started: true
  }
  #locale: null
  #defaultLocale: "en"
  -preferredFormat: null
  -isHostValid: true
  -isForwardedValid: true
  #json: null
  #convertedFiles: []
  #userResolver: Closure($guard = null)^ {#3220
    class: "Illuminate\Auth\AuthServiceProvider"
    this: Illuminate\Auth\AuthServiceProvider {#451 …}
    use: {
      $app: Illuminate\Foundation\Application {#1115 …}
    }
  }
  #routeResolver: Closure()^ {#3241
    class: "Illuminate\Routing\Router"
    this: Illuminate\Routing\Router {#1206 …}
    use: {
      $route: Illuminate\Routing\Route {#1397 …}
    }
  }
  basePath: ""
  format: "html"
}
tykus's avatar

@jmacdiarmid you are running a test; when do you check the database for the saved data? What does the test class look like?

jmacdiarmid's avatar

@tykus Here's the test class:

class BookReservationTest extends TestCase
{
    use RefreshDatabase;
    use WithFaker;

    public function __construct(string $name)
    {
        parent::__construct($name);

        $this->setUpFaker();

    }

    public function testABookCanBeAddedToTheLibrary(): void
    {
        $this->withoutExceptionHandling();

        /** @var User $user */
        $user = User::factory()->create();

        $this->actingAs($user);

//        $this->assertAuthenticated();

        $response = $this->post('/books/store', [
            'title' => $this->faker->sentence(6, true),
//            'cover' => $this->faker->image(null, 1600, 2560, null,null,true),
            'content' => Lorem::paragraph(3, true),
            'price' => $this->faker->numberBetween($min = 500, $max = 10099),
            'year_published' => $this->faker->numberBetween($min = 1700, $max = 2023),
        ]);

//        $this->assertTrue(true);
//        $response->assertRedirect(RouteServiceProvider::HOME);
        $response->assertOk();
//        $response->assertStatus($response->status);
        $this->assertCount(1, Book::all());
    }
}
jmacdiarmid's avatar

What is the difference between calling the store method on a controller to write records to the database from a TDD test and calling the method from a form? I've been trying everything I can think of. I've also compared how I'm storing data in the database with how I'm doing the same in other projects that work fine.

In this project, I've also tried to save using the create method. Doesn't work. I just tried it doing this:

        $book = new Book([
            'title' => $request->get('title'),
            'content' => $request->get('content'),
            'price' => $request->get('price'),
            'year_published' => $request->get('year_published'),
            'author_id' => $request->get('author_id'),
        ]);

        $book->save();
        $fetchedBook = Book::find($book->id);
        dd($fetchedBook);

Here's what I got back :

App\Models\Book^ {#3301 // app\Http\Controllers\BookController.php:74
  #connection: "mysql"
  #table: "books"
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: []
  #withCount: []
  +preventsLazyLoading: false
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: false
  #escapeWhenCastingToString: false
  #attributes: array:9 [
    "id" => 1
    "title" => "Quis eum dolores illo a."
    "content" => "Voluptatum ipsam facilis ea vero qui ut distinctio. Voluptas quisquam est facilis inventore qui eum. Nobis aut adipisci quia nemo dolores dolorem molestiae. Autem omnis eos ut ut inventore laboriosam."
    "price" => "5487.00"
    "year_published" => "1900"
    "cover" => null
    "author_id" => 76
    "created_at" => "2023-07-17 20:18:11"
    "updated_at" => "2023-07-17 20:18:11"
  ]
  #original: array:9 [
    "id" => 1
    "title" => "Quis eum dolores illo a."
    "content" => "Voluptatum ipsam facilis ea vero qui ut distinctio. Voluptas quisquam est facilis inventore qui eum. Nobis aut adipisci quia nemo dolores dolorem molestiae. Autem omnis eos ut ut inventore laboriosam."
    "price" => "5487.00"
    "year_published" => "1900"
    "cover" => null
    "author_id" => 76
    "created_at" => "2023-07-17 20:18:11"
    "updated_at" => "2023-07-17 20:18:11"
  ]
  #changes: []
  #casts: []
  #classCastCache: []
  #attributeCastCache: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: []
  #touches: []
  +timestamps: true
  +usesUniqueIds: false
  #hidden: []
  #visible: []
  #fillable: array:6 [
    0 => "title"
    1 => "content"
    2 => "price"
    3 => "year_published"
    4 => "cover"
    5 => "author_id"
  ]
  #guarded: array:1 [
    0 => "*"
  ]
}

I also tried

Book::all()->count()

It returns: 1

Seeding the table works fine using a factory class.

Snapey's avatar

@jmacdiarmid one difference is that you are using Mass Assignment in your test, and not in your controller.

Snapey's avatar

change your saveOrFail to just save and then the error should be revealed (assuming the code is as you show, if the save fails, you return with no error or message)

jmacdiarmid's avatar

@Snapey I changed it back to save, still not saving but now it gives a 302 - Redirect.

I've discovered that the test works with a successful save when I have use RefreshDatabase commented out.

tykus's avatar

When are you checking that the data is written to the database that informs the but no records are written to the database statement? When you dd($fetchedBook);, you get a record that exists?

jmacdiarmid's avatar

@tykus Yes, it was saying that records exist, but when I refreshed the database view in phpmyadmin, it was not showing records in the table. As I mentioned in my previous reply to @snapey, when I use the RefreshDatabase use statement that's when no records are shown as if they are getting cleared after the save.

jmacdiarmid's avatar

Ok, everything is working now as expected. After further research, I had to make some "adjustments" to my testcase. I'm not using use RefreshDatabase so I created my own database refresh. Here's what I did:

I added the setupBeforeClass() hook

    public static function setUpBeforeClass(): void
    {
        $config = parse_ini_file('.env');
        $userName = $config['DB_USERNAME'];
        $password = $config['DB_PASSWORD'];
        $database = $config['DB_DATABASE'];
        $host = $config['DB_HOST'];

        $connection = new PDO("mysql:host={$host}", $userName, $password);
        $connection->query("DROP DATABASE IF EXISTS `{$database}`;");
        $connection->query("CREATE DATABASE `{$database}`;");
    }

then, in the testcase function, I added the following before creating a user and creating the db record.

 Artisan::call('migrate');
tykus's avatar

@jmacdiarmid RefreshDatabase also works! Does your code not effectively perform the same task albeit a tick later? Why do you want to manually inspect the records in your test database during the course of TDD cycles?

jmacdiarmid's avatar

@tykus True, the RefreshDatabase trait works. However it wasn't working as I had expected it to. I wanted to make sure I wasn't loosing my mind as well. :)

Please or to participate in this conversation.