/** * Pushes the post to the specified board, as a new thread or as a reply. * This autoatically handles concurrency issues. Creating a new reply without * using this method is forbidden by the `creating` event in ::boot. * * * @param App\Board &$board * @param App\Post &$thread * @return void */ public function submitTo(Board &$board, &$thread = null) { $this->board_uri = $board->board_uri; $this->author_ip = inet_pton(Request::ip()); $this->reply_last = $this->freshTimestamp(); $this->bumped_last = $this->reply_last; $this->setCreatedAt($this->reply_last); $this->setUpdatedAt($this->reply_last); if (!is_null($thread) && !$thread instanceof Post) { $thread = $board->getLocalThread($thread); $this->reply_to = $thread->post_id; $this->reply_to_board_id = $thread->board_id; } // Store the post in the database. DB::transaction(function () use($board, $thread) { // The objective of this transaction is to prevent concurrency issues in the database // on the unique joint index [`board_uri`,`board_id`] which is generated procedurally // alongside the primary autoincrement column `post_id`. // First instruction is to add +1 to posts_total and set the last_post_at on the Board table. DB::table('boards')->where('board_uri', $this->board_uri)->increment('posts_total'); DB::table('boards')->where('board_uri', $this->board_uri)->update(['last_post_at' => $this->created_at]); // Second, we record this value and lock the table. $boards = DB::table('boards')->where('board_uri', $this->board_uri)->lockForUpdate()->select('posts_total')->get(); $posts_total = $boards[0]->posts_total; // Optionally, the OP of this thread needs a +1 to reply count. if ($thread instanceof Post) { if (!$this->isBumpless() && !$thread->isBumplocked()) { $thread->bumped_last = $this->created_at; // We explicitly set the updated_at to what it is now. // If we didn't, this would change. // We don't want that because it screws up the API and // makes it think the OP post has had its content edited. $thread->updated_at = $thread->updated_at; } $thread->reply_last = $this->created_at; $thread->reply_count += 1; $thread->save(); } // Optionally, we also expend the adventure. $adventure = BoardAdventure::getAdventure($board); if ($adventure) { $this->adventure_id = $adventure->adventure_id; $adventure->expended_at = $this->created_at; $adventure->save(); } // Finally, we set our board_id and save. $this->board_id = $posts_total; $this->author_id = $this->makeAuthorId(); $this->save(); // Queries and locks are handled automatically after this closure ends. }); // Process uploads. $uploads = []; if (is_array($files = Input::file('files'))) { $uploads = array_filter($files); } if (count($uploads) > 0) { foreach ($uploads as $uploadIndex => $upload) { if (file_exists($upload->getPathname())) { FileStorage::createAttachment($upload, $this); } } } // Finally fire event on OP, if it exists. if ($thread instanceof Post) { Event::fire(new ThreadNewReply($thread)); } }