@Strider64, commenting on your code.
One of the points of using the PDO extension is that it can be simpler to use than the mysqli extension.
The only database exceptions you should catch and handle in your code are for user recoverable errors, such as when inserting/updating user submitted values. All other database statement errors are due to programming mistakes or the database server not running, …, which the user (hacker) visiting a site doesn’t need to know anything about. If you do NOTHING in your code for these cases and simply let php catch and handle any database exception, php will ‘automatically’ display/log the raw database statement errors the same as php errors, via an uncaught exception error.
By catching and throwing your own exception, with only the message and code (this is the sql state code, which usually corresponds to several related errors), you loose the file name, line number, and error number information, making any debugging harder. If you do nothing in this case, the information php displays or logs will include all the available information.
By specifically binding inputs, in addition to unnecessary lines of code, you are limiting values to php’s maximums, which are much smaller than the values that databases support. While this won’t immediately cause problems in academic assignments, it can result in unexplained data issues in real applications. Since your code is using real prepared queries, if you simply supply an array of the input values to the ->execute([…]) call, this respects the data type of the value. This works correctly for strings, numbers, boolean, null, and blog/binary data.
RowCount() is not guaranteed to be supported for queries that return result sets. Just fetch all the data from a query and test (or count()) if the fetched data is a true/false value.
You have set the default fetch mode to assoc when you make the database connection. Why are you specifying it in the fetch() statement?