diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..0f922b1c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,134 @@ + +# MagicObject Version 2 + +## What's New + +**MagicObject Version 2** brings several exciting new features and enhancements aimed at increasing flexibility, improving performance, and providing better control over database interactions. Below are the key updates introduced in this version: + +### 1. Native Query Support + +**Native SQL Queries** are now supported in **MagicObject**, allowing users to execute raw SQL statements directly within the framework. This enhancement gives developers greater control over complex queries, which may not be easily handled by the ORM layer. You can now execute any SQL command directly, enabling the use of advanced SQL features and custom queries that go beyond the capabilities of the built-in ORM. + +### 2. Multiple Database Connection Support + +The new version of **MagicObject** introduces the ability to configure and manage **multiple database connections** within a single application. This feature allows developers to easily connect to and manage different databases simultaneously, making it ideal for applications that need to interact with multiple databases or implement multi-database architectures. Whether you're working with multiple MySQL instances, different types of databases (e.g., PostgreSQL, SQLite), or managing different environments (development, production), this feature significantly simplifies database management. + +### 3. Entity Cache Control on Joins + +**MagicObject Version 2** gives developers greater control over **entity caching** when performing **join operations**. The new feature allows you to enable or disable caching specifically for joins, providing fine-tuned control over your caching strategy. This improves performance by reducing unnecessary database hits while still ensuring fresh data retrieval when needed. You can now optimize caching on a per-query basis, making it easier to manage large data sets efficiently. + +### 4. Enhanced Documentation + +The documentation for **MagicObject** has been thoroughly updated. In this release, we've made significant improvements to the documentation for **classes**, **properties**, **functions**, and **annotations**. The documentation now includes clearer explanations, improved examples, and comprehensive usage guidelines. These changes are designed to make it easier for developers to understand and fully leverage the power of the framework, reducing the learning curve and streamlining the development process. + +### 5. Bug Fixes and Stability Enhancements + +Several bugs and issues from previous versions have been addressed in **MagicObject Version 2**. This includes improvements to **performance**, **stability**, and the **correction of minor errors** that may have affected the functionality of the framework. With these fixes, users can expect a more reliable and robust framework that performs well across a variety of use cases. + +## Additional Features + +- **Improved Error Handling**: We've introduced enhanced mechanisms for detecting and handling errors. The error messages are now more informative, helping developers to troubleshoot and resolve issues faster. This improvement also includes better stack trace information and more specific error types. + +- **Performance Optimizations**: Internally, **MagicObject Version 2** has been optimized to improve overall performance. Key database interaction operations have been streamlined, leading to faster query execution times and better resource utilization. + +- **Backward Compatibility**: **MagicObject Version 2** maintains **backward compatibility** with **Version 1**, ensuring that existing users can upgrade smoothly without having to make significant changes to their codebase. This allows for an easy transition to the new version while still maintaining compatibility with legacy systems. + + +## Migration Notes + +If you are upgrading from **MagicObject Version 1** to **Version 2**, please review the migration notes carefully. The documentation includes detailed guidelines and best practices for handling any potential breaking changes, as well as adjustments that may be necessary to ensure a smooth transition. By following these guidelines, you can ensure that your upgrade process is as seamless as possible, minimizing disruptions to your development workflow. + + + +# MagicObject Version 2.1 + +## What's New + +**MagicObject 2.1** introduces several powerful new features aimed at improving entity management, database interoperability, and overall ease of use. This version builds on the foundational updates from previous releases, making database handling even more efficient and developer-friendly. Here’s a detailed overview of the new additions: + +### 1. Package Annotations for Entity Joins + +One of the most notable features in **MagicObject 2.1** is the introduction of **package annotations** for entities. These annotations are essential when joining entities, as they provide the necessary namespace information that is critical for the framework to correctly recognize and associate entity classes. + +#### Why Package Annotations? + +PHP does not natively provide a way to directly retrieve the namespace of a class, which presented a challenge for earlier versions of **MagicObject** when attempting to perform joins. To work around this, **MagicObject** previously attempted to infer namespaces by reading the PHP script, but this method proved to be both inefficient and prone to errors. + +With **MagicObject 2.1**, the introduction of package annotations on each entity allows the framework to safely and efficiently join entities by referencing the class's base name, without needing to manually specify or infer the namespace. This makes the process of joining entities more robust and reliable. + +#### Backwards Compatibility + +If a package annotation is not present on an entity, **MagicObject 2.1** will gracefully revert to the old method of namespace inference, ensuring backwards compatibility with previous versions. However, it is strongly recommended to utilize the new package annotations for better performance and accuracy when performing entity joins. + +### 2. Seamless Database Conversion Between PostgreSQL and MySQL + +**MagicObject 2.1** introduces a powerful utility that allows developers to seamlessly convert databases between **PostgreSQL** and **MySQL**. This feature greatly simplifies the process of migrating applications and data between these two popular database systems. + +#### Key Features of Database Conversion: + +- **Data Type Mapping**: MagicObject handles the conversion of data types between PostgreSQL and MySQL, ensuring that the equivalent types are correctly mapped. +- **Constraints and Structures**: The conversion tool also accounts for database constraints, indexes, and table structures, ensuring that the integrity of the database schema is maintained during migration. +- **Error Reduction**: By automating the conversion process, MagicObject reduces the chances of errors that can occur during manual migration, saving time and effort for developers. + +This new functionality provides developers with a simple and efficient way to migrate data between PostgreSQL and MySQL, which is particularly useful for projects that need to switch databases or support multiple database systems. + +### 3. Parsing Table Structures from SQL Statements + +Another significant enhancement in **MagicObject 2.1** is the ability to **parse table structures directly from SQL statements**. Developers no longer need to first dump the schema into a database before they can interact with it. Instead, MagicObject allows you to read and manipulate the structure of a database directly from SQL. + +#### Benefits of Table Structure Parsing: + +- **Streamlined Workflow**: This feature eliminates the need for a two-step process (first dumping, then reading the schema) and allows developers to work more directly with SQL code. +- **Integrate with Third-Party Systems**: Developers can now easily parse and manipulate schemas from third-party systems that provide SQL code, without needing to import the data into a database first. +- **Improved Efficiency**: This utility speeds up the process of understanding and working with complex database schemas, making it easier to integrate and maintain SQL-driven projects. + +By providing a direct way to parse table structures, **MagicObject 2.1** significantly simplifies database schema management and makes it more accessible, especially for developers working with raw SQL or third-party integrations. + +## Summary + +**MagicObject 2.1** brings a suite of powerful features designed to enhance database management, simplify entity relationships, and improve the overall development process. Key updates include: + +- **Package annotations** for safer and more efficient entity joins. +- A **seamless database conversion tool** between PostgreSQL and MySQL, simplifying migrations. +- **Direct parsing of SQL table structures**, eliminating the need for intermediate steps. + +These updates significantly improve the flexibility and efficiency of **MagicObject**, making it even easier for developers to manage databases and integrate with various systems. With **MagicObject 2.1**, developers can focus more on building applications and less on wrestling with database compatibility and entity management. + + +# MagicObject Version 2.7 + +## What's New + +**MagicObject 2.7** brings a set of powerful updates to improve database interaction, query flexibility, and transaction management. The main highlights of this release include support for PDO connections, enhanced native query capabilities with pagination and sorting, and new transactional methods for improved data management. + +### 1. PDO Support + +One of the most significant changes in **MagicObject 2.7** is the introduction of support for **PDO** (PHP Data Objects). In previous versions, **MagicObject** required the use of its custom database handler, **PicoDatabase**. However, to accommodate developers who prefer working with PDO connections, this new version allows users to pass a PDO connection directly to the **MagicObject** constructor. + +#### Why PDO Support? + +The decision to include PDO support was driven by the need to make **MagicObject** more versatile for developers who are already using PDO in their applications. By allowing PDO connections, **MagicObject** now supports a broader range of use cases and provides users with the flexibility to integrate with existing PDO-based database connections. + +While PDO is supported for the initial connection setup, **MagicObject** continues to use **PicoDatabase** for all subsequent database operations. This ensures that users still benefit from **PicoDatabase**'s advanced features, such as automatic query building, database abstraction, and optimized query execution. + +#### How PDO Support Works + +In **MagicObject 2.7**, when you pass a **PDO** connection object to the constructor, it is internally converted into a **PicoDatabase** instance via the `PicoDatabase::fromPdo()` static method. This ensures that although PDO is used for establishing the initial connection, **PicoDatabase** manages the actual database interactions. Additionally, **MagicObject** automatically detects the database type based on the PDO driver to ensure smooth operation. + +### 2. Pageable and Sortable in Native Queries + +Another important enhancement in **MagicObject 2.7** is the introduction of **pageable** and **sortable** support in native queries. Prior to this release, native queries lacked direct support for pagination and sorting. Developers had to manually include `ORDER BY` and `LIMIT OFFSET` clauses in their queries, leading to more cumbersome code that was difficult to maintain and adapt across different database platforms. + +With **MagicObject 2.7**, you can now pass **pagination** parameters using the `PicoPageable` type and **sorting** parameters using the `PicoSortable` type directly into your native queries. These parameters can be placed at any point in the query, though it's recommended to position them either at the beginning or end for optimal readability and organization. + +This improvement enhances the flexibility of native queries, as the logic for pagination and sorting is handled automatically, reducing the need for manual intervention. By supporting these features, **MagicObject 2.7** allows you to write cleaner, more efficient, and database-agnostic queries. You can now easily handle pagination and sorting logic regardless of the underlying database system. + +### 3. Transaction Management + +**MagicObject 2.7** introduces enhanced support for transactional database operations, including three new methods: `startTransaction()`, `commit()`, and `rollback()`. These methods provide an easy and efficient way to manage database transactions within **MagicObject**. + +- **startTransaction()**: Begins a new database transaction. +- **commit()**: Commits the current transaction, saving all changes made during the transaction to the database. +- **rollback()**: Rolls back the current transaction, undoing any changes made since the transaction began. + +These methods are designed to work seamlessly with an active database connection, allowing developers to handle transactions directly within the context of their application. Whether you're managing financial transactions or ensuring data consistency during batch processing, these functions streamline the management of transaction-based operations. diff --git a/README.md b/README.md index b0156454..f19302ec 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,35 @@ Additionally, MagicObject 2.1 allows users to parse table structures directly fr These utilities not only enhance efficiency but also provide a robust foundation for database development, allowing users to focus on building applications rather than wrestling with database compatibility issues. With MagicObject 2.1, database management becomes more intuitive and accessible, empowering developers to harness the full potential of their data. + +# **PDO Support in MagicObject 2.7** + +## **Overview** + +With the release of **MagicObject 2.7**, a significant update has been introduced to allow users to leverage **PDO** (PHP Data Objects) for database connections. In previous versions, **MagicObject** required the use of **PicoDatabase**, its custom database handling class. However, recognizing that many developers are accustomed to establishing database connections via traditional PDO, this new version introduces flexibility by allowing PDO connections to be passed directly to the **MagicObject** constructor. + +This update aims to bridge the gap between traditional PDO-based database management and the advanced features provided by **MagicObject**, thus enhancing compatibility while retaining all the powerful functionality of the framework. + +## **Why PDO Support?** + +The decision to support **PDO** was made to accommodate users who have already established database connections in their applications using PDO, instead of relying on **PicoDatabase** from the start. By supporting PDO, **MagicObject** allows users to continue working with their preferred method of connecting to the database while still benefiting from the full range of features and utilities **MagicObject** offers. + +While PDO is now an option for initializing **MagicObject**, it is used only in the constructor. Once the object is initialized, **MagicObject** continues to use **PicoDatabase** for all subsequent database interactions, ensuring that users can still benefit from **PicoDatabase**'s advanced features like automatic query building, database abstraction, and optimized query execution. + +## **How PDO Support Works** + +In **MagicObject 2.7**, when you pass a **PDO** connection object to the constructor, it is automatically converted into a **PicoDatabase** instance using the `PicoDatabase::fromPdo()` static method. This ensures that even though PDO is used to establish the initial connection, the object will still operate using **PicoDatabase** for all subsequent database operations. The constructor of **MagicObject** ensures that the database connection is properly initialized and the type of database is correctly detected based on the PDO driver. + +# **Pageable and Sortable in Native Query in MagicObject 2.7** + +In **MagicObject version 2.7**, support for **pageable** and **sortable** functionality has been added to native queries. Previously, native queries did not support pagination and sorting directly. Instead, users had to manually include `SORT BY` and `LIMIT OFFSET` clauses in their queries, which made them less flexible. This approach was problematic because each Database Management System (DBMS) has its own syntax for writing queries, making it cumbersome to adapt queries for different platforms. + +With the introduction of pageable and sortable support in version 2.7, users can now easily pass **pagination** parameters using the `PicoPageable` type and **sorting** parameters using the `PicoSortable` type directly into their native queries. These parameters can be placed anywhere within the query, but it is recommended to position them either at the beginning or the end of the query for optimal readability and organization. + +This enhancement makes native queries more flexible and easier to maintain, as the logic for pagination and sorting is handled automatically, without requiring manual intervention for each DBMS. As a result, users can now write cleaner, more efficient, and database-agnostic native queries. + + + # Tutorial Tutorial is provided here https://github.com/Planetbiru/MagicObject/blob/main/tutorial.md diff --git a/manual/includes/_database.md b/manual/includes/_database.md index 2c35af3a..768f1fe0 100644 --- a/manual/includes/_database.md +++ b/manual/includes/_database.md @@ -14,6 +14,28 @@ - **Callbacks**: Support for custom callback functions for query execution and debugging. - **Unique ID Generation**: Generate unique identifiers for database records. +### Database Support + +MagicObject supports the following databases: + +1. **MySQL** + + One of the most popular open-source relational databases, known for its speed, reliability, and ease of use. MySQL is widely used in web applications and offers strong performance, security features, and support for SQL standards. + +2. **MariaDB** + + A fork of MySQL, created by the original developers of MySQL after concerns over Oracle’s acquisition of MySQL. MariaDB is designed to maintain compatibility with MySQL while adding new features and optimizations. It is fully open-source and highly regarded for its performance and stability. + +3. **PostgreSQL** + + A powerful, open-source relational database system known for its robustness, SQL compliance, and extensive feature set, including ACID compliance, JSON support, and advanced indexing mechanisms. + +4. **SQLite** + + A lightweight, serverless, self-contained SQL database engine that is highly portable. It is often used for embedded systems or small-scale applications due to its minimal setup and resource usage. Despite its simplicity, SQLite supports a wide range of SQL features and is widely used in mobile apps and other local storage scenarios. + +MagicObject’s compatibility with these databases enables flexible, scalable, and efficient data management across different platforms and environments. + ### Installation To use the `PicoDatabase` class, ensure you have PHP with PDO support. Include the class file in your project, and you can instantiate it with your database credentials. @@ -243,7 +265,7 @@ public function query($sql, $params = null) - `array|null $params`: Optional parameters for the SQL query. **Returns**: PDOStatement object or `false` on failure. -##### Fetch a Single Result +#### Fetch a Single Result ```php public function fetch($sql, $tentativeType = PDO::FETCH_ASSOC, $defaultValue = null, $params = null) diff --git a/manual/includes/_download-file.md b/manual/includes/_download-file.md new file mode 100644 index 00000000..2aaf071f --- /dev/null +++ b/manual/includes/_download-file.md @@ -0,0 +1,96 @@ + +## Resumable File Download + +### Namespace + +`MagicObject\File` + +### Description + +The `PicoDownloadFile` class is designed to facilitate efficient file downloading in PHP, supporting **partial content** (range requests) for large files. It ensures that requested files exist, handles errors gracefully, and enables downloading in chunks to minimize server load and bandwidth consumption, particularly for large files. + +The class supports the following: + +- Verifying the existence of the file. +- Handling byte-range requests for resuming downloads. +- Sending appropriate HTTP headers to manage the download. +- Streaming the file to the client in manageable chunks (default size: 8 KB). +- Returning relevant HTTP status codes and error messages. + +This class is ideal for scenarios where large files need to be served to clients and you want to offer functionality like resuming interrupted downloads. + + +### Constructor + +```php +__construct($filepath, $filename = null) +``` + +**Parameters**: + +- `$filepath` (string): The full path to the file that should be downloaded. +- `$filename` (string|null, optional): The name of the file for download. If not provided, the filename is extracted from the `filepath` using `basename()`. + +**Description**: Initializes the `PicoDownloadFile` object with the path of the file to be downloaded and an optional filename for the download response. If the filename is not specified, the base name of the file is used. + +**Example**: + +```php +$file = new PicoDownloadFile("/path/to/large-file.zip", "downloaded-file.zip"); +``` + +### Method + +```php +download($exit = false) +``` + +**Parameters**: + +- `$exit` (bool, optional): Whether to terminate the script after sending the file. Default is `false`. + +**Returns**: + +- `bool`: Returns `true` if the entire file was successfully sent, `false` if only part of the file was sent (due to range requests). + +**Description**: This method is responsible for initiating the file download process. It performs the following: + +1. Verifies the existence of the file. +2. Handles byte-range requests for partial downloads (useful for resuming interrupted downloads). +3. Sends the appropriate HTTP headers for the file download. +4. Streams the file to the client in chunks of 8 KB (by default). + +If `$exit` is set to `true`, the script will terminate after the file is sent. + +**Example 1** + +```php +download(true); // Initiate download and terminate the script after sending +``` + +**Example 2** + +```php +download(false); // Initiate download without terminate the script after sending +if($finished && file_exists($path)) +{ + unlink($path); // Delete file when finish +} +``` + +### Error Handling + +- **404 - File Not Found**: If the file does not exist at the specified path, a 404 error is returned. +- **416 - Range Not Satisfiable**: If an invalid byte range is requested (e.g., the start byte is larger than the end byte), a 416 error is returned. +- **500 - Internal Server Error**: If there is an issue opening the file for reading (e.g., permissions issues), a 500 error is returned. + diff --git a/manual/includes/_entity.md b/manual/includes/_entity.md index 474efaa8..0cdcdbb4 100644 --- a/manual/includes/_entity.md +++ b/manual/includes/_entity.md @@ -2,6 +2,9 @@ Entity is class to access database. Entity is derived from MagicObject. Some annotations required to activated all entity features. +MagicObject version 2.7 introduces new features for transactional database management, namely `startTransaction()`, `commit()`, and `rollback()`. These functions allow entities to directly initiate and manage transactions within their scope. The `startTransaction()` function begins a new transaction, while `commit()` ensures that all changes made during the transaction are permanently saved to the database. On the other hand, `rollback()` can be used to revert any changes made during the transaction in case of an error or interruption. These functions require an active database connection to operate, providing a streamlined way for entities to manage data consistency and integrity within their transactions. + + **Constructor** Parameters: diff --git a/manual/includes/_native-query.md b/manual/includes/_native-query.md index 8425921d..b0a574dd 100644 --- a/manual/includes/_native-query.md +++ b/manual/includes/_native-query.md @@ -63,6 +63,14 @@ Native query must be a function of a class that extends from the MagicObject cla Native queries can be created on entities used by the application. If in the previous version the entity only contained properties, then in version 2.0, the entity can also contain functions for native queries. However, entities in versions 1 and 2 both support functions but functions with native queries are only supported in version 2.0. +### Pagination and Sorting + +In **MagicObject version 2.7**, support for **pageable** and **sortable** functionality has been added to native queries. Previously, native queries did not support pagination and sorting directly. Instead, users had to manually include `SORT BY` and `LIMIT OFFSET` clauses in their queries, which made them less flexible. This approach was problematic because each Database Management System (DBMS) has its own syntax for writing queries, making it cumbersome to adapt queries for different platforms. + +With the introduction of pageable and sortable support in version 2.7, users can now easily pass **pagination** parameters using the `PicoPageable` type and **sorting** parameters using the `PicoSortable` type directly into their native queries. These parameters can be placed anywhere within the query, but it is recommended to position them either at the beginning or the end of the query for optimal readability and organization. + +This enhancement makes native queries more flexible and easier to maintain, as the logic for pagination and sorting is handled automatically, without requiring manual intervention for each DBMS. As a result, users can now write cleaner, more efficient, and database-agnostic native queries. + ### Debug Query MagicObject checks if the database connection has a debugging function for queries. If available, it sends the executed query along with the parameter values to this function, aiding users in identifying errors during query definition and execution. @@ -338,6 +346,28 @@ class Supervisor extends MagicObject // Call parent method to execute the query return $this->executeNativeQuery(); } + + /** + * Native query 13 + * + * This method will return a prepared statement for further operations if necessary. + * + * @param PicoPagebale $pageable + * @param PicoSortable $sortable + * @param bool $aktif The active status to filter results. + * @return MagicObject[] + * @query(" + SELECT supervisor.* + FROM supervisor + WHERE supervisor.supervisor_id in :supervisorId + AND supervisor.aktif = :aktif + ") + */ + public function native13($pageable, $sortable, $aktif) + { + // Call parent method to execute the query + return $this->executeNativeQuery(); + } } $obj = new Supervisor(null, $database); @@ -391,6 +421,25 @@ echo "Alamat: " . $native8->getTelepon() . "\r\n"; echo "Alamat: " . $native9[0]->getTelepon() . "\r\n"; echo "Alamat: " . $native10->getTelepon() . "\r\n"; echo "Alamat: " . $native11[0]->getTelepon() . "\r\n"; + + +$sortable = new PicoSortable(); +$sortable->addSortable(new PicoSort("nama", PicoSort::ORDER_TYPE_ASC)); +$pageable = new PicoPageable(new PicoPage(3, 20)); + +try +{ + $native13 = $obj->native13($pageable, $sortable, true); + echo "\r\nnative13:\r\n"; + foreach($native13 as $sup) + { + echo $sup."\r\n\r\n"; + } +} +catch(Exception $e) +{ + echo $e->getMessage(); +} ``` For the purpose of exporting large amounts of data, use the PDOStatement return type. PDOStatement allows users to read one by one and process it immediately, allowing PHP to release memory from the previous process. PHP does not need to store very large data in a variable. diff --git a/manual/includes/_upload-file.md b/manual/includes/_upload-file.md index d51a14bb..ed2b570f 100644 --- a/manual/includes/_upload-file.md +++ b/manual/includes/_upload-file.md @@ -1,4 +1,4 @@ -## Upload File +## File Upload ### Overview diff --git a/manual/includes/_with-pdo.md b/manual/includes/_with-pdo.md new file mode 100644 index 00000000..cef2c4f2 --- /dev/null +++ b/manual/includes/_with-pdo.md @@ -0,0 +1,30 @@ + +## MagicObject with PDO + +### Overview + +With the release of **MagicObject 2.7**, a significant update has been introduced to allow users to leverage **PDO** (PHP Data Objects) for database connections. In previous versions, **MagicObject** required the use of **PicoDatabase**, its custom database handling class. However, recognizing that many developers are accustomed to establishing database connections via traditional PDO, this new version introduces flexibility by allowing PDO connections to be passed directly to the **MagicObject** constructor. + +This update aims to bridge the gap between traditional PDO-based database management and the advanced features provided by **MagicObject**, thus enhancing compatibility while retaining all the powerful functionality of the framework. + +### Why PDO Support? + +The decision to support **PDO** was made to accommodate users who have already established database connections in their applications using PDO, instead of relying on **PicoDatabase** from the start. By supporting PDO, **MagicObject** allows users to continue working with their preferred method of connecting to the database while still benefiting from the full range of features and utilities **MagicObject** offers. + +While PDO is now an option for initializing **MagicObject**, it is used only in the constructor. Once the object is initialized, **MagicObject** continues to use **PicoDatabase** for all subsequent database interactions, ensuring that users can still benefit from **PicoDatabase**'s advanced features like automatic query building, database abstraction, and optimized query execution. + +### How PDO Support Works + +In **MagicObject 2.7**, when you pass a **PDO** connection object to the constructor, it is automatically converted into a **PicoDatabase** instance using the `PicoDatabase::fromPdo()` static method. This ensures that even though PDO is used to establish the initial connection, the object will still operate using **PicoDatabase** for all subsequent database operations. The constructor of **MagicObject** ensures that the database connection is properly initialized and the type of database is correctly detected based on the PDO driver. + +### Benefits of PDO Support in MagicObject 2.7 + +- **Compatibility**: This change makes **MagicObject** more compatible with existing applications that are already using PDO for database connections. Developers can continue to use PDO for initializing connections while taking advantage of **PicoDatabase**'s advanced database features for the rest of the application. + +- **Flexibility**: Developers now have the flexibility to choose between traditional PDO connections and **PicoDatabase**, depending on their needs. This is especially useful for applications transitioning to **MagicObject** but needing to maintain compatibility with existing database handling code. + +- **Ease of Transition**: By supporting PDO in the constructor, **MagicObject** makes it easier for developers to gradually adopt its features without the need to refactor existing database handling code. + +### Conclusion + +Version 2.7 of **MagicObject** introduces an important enhancement by allowing PDO connections to be used alongside **PicoDatabase**. This update provides greater flexibility for developers, allowing them to work with traditional PDO connections if they choose, while still benefiting from the advanced features of **MagicObject** for database interactions. This change aligns with the goal of making **MagicObject** more accessible to a wider range of developers, whether they are just starting with **MagicObject** or are looking to transition from an existing PDO-based application. diff --git a/manual/index.html b/manual/index.html index 599e982c..ff2c4539 100644 --- a/manual/index.html +++ b/manual/index.html @@ -2082,6 +2082,27 @@

Features

  • Callbacks: Support for custom callback functions for query execution and debugging.
  • Unique ID Generation: Generate unique identifiers for database records.
  • +

    Database Support

    +

    MagicObject supports the following databases:

    +
      +
    1. +

      MySQL

      +

      One of the most popular open-source relational databases, known for its speed, reliability, and ease of use. MySQL is widely used in web applications and offers strong performance, security features, and support for SQL standards.

      +
    2. +
    3. +

      MariaDB

      +

      A fork of MySQL, created by the original developers of MySQL after concerns over Oracle’s acquisition of MySQL. MariaDB is designed to maintain compatibility with MySQL while adding new features and optimizations. It is fully open-source and highly regarded for its performance and stability.

      +
    4. +
    5. +

      PostgreSQL

      +

      A powerful, open-source relational database system known for its robustness, SQL compliance, and extensive feature set, including ACID compliance, JSON support, and advanced indexing mechanisms.

      +
    6. +
    7. +

      SQLite

      +

      A lightweight, serverless, self-contained SQL database engine that is highly portable. It is often used for embedded systems or small-scale applications due to its minimal setup and resource usage. Despite its simplicity, SQLite supports a wide range of SQL features and is widely used in mobile apps and other local storage scenarios.

      +
    8. +
    +

    MagicObject’s compatibility with these databases enables flexible, scalable, and efficient data management across different platforms and environments.

    Installation

    To use the PicoDatabase class, ensure you have PHP with PDO support. Include the class file in your project, and you can instantiate it with your database credentials.

    use MagicObject\Database\PicoDatabase;
    @@ -2283,7 +2304,7 @@ 

    Query Execution

  • array|null $params: Optional parameters for the SQL query. Returns: PDOStatement object or false on failure.
  • -
    Fetch a Single Result
    +

    Fetch a Single Result

    public function fetch($sql, $tentativeType = PDO::FETCH_ASSOC, $defaultValue = null, $params = null)

    Parameters:

    -
    +

    Specification

    Specifications are implemented in the PicoSpecification and PicoPredicate classes. PicoSpecification is a framework that can contain one or more PicoPredicate.

    For example, we have the following query:

    @@ -7325,7 +7372,7 @@

    Specification

    }
    -
    +

    Pageable and Sortable

    In MagicObject, pageable is used to divide data rows into several pages. This is required by the application to display a lot of data per page. While sortable is used to sort data before the data is divided per page.

    Pageable can stand alone without sortable. However, this method is not recommended because the data sequence is not as expected. If new data is entered, users will have difficulty finding where it is located in the list and on which page the data will appear. The solution is to add a sortable that will sort the data based on certain columns. For example, the time of data creation is descending, then the new data will be on the first page. Conversely, if sorted based on the time of data creation is ascending, then the new data will be on the last page.

    @@ -7617,7 +7664,7 @@

    Pageable and Sortable

    ORDER BY user_name ASC, email DESC, phone ASC LIMIT 200 OFFSET 400

    -
    +

    Filtering, Ordering and Pagination

    MagicObject will filter data according to the given criteria. On the other hand, MagicObject will only retrieve data on the specified page by specifying limit and offset data in the select query.

    Example parameters:

    @@ -8500,7 +8547,7 @@

    Filtering, Ordering and Pagination

    ?>
    -
    +

    Native Query

    In MagicObject version 2, native queries have been introduced as an efficient way to interact with the database.

    Native queries offer significant performance improvements when handling large volumes of data, allowing users to craft highly efficient queries that meet diverse requirements.

    @@ -8556,6 +8603,10 @@

    Return Type

    If there is an error executing the database query, a PDOException will be thrown.

    Native query must be a function of a class that extends from the MagicObject class. In its definition, this method must call $this->executeNativeQuery(). MagicObject::executeNativeQuery() will analyze the docblock, parameters, and return type to process the given query. For ease and flexibility in writing code, the MagicObject::executeNativeQuery() function call does not pass parameters. Instead, the MagicObject::executeNativeQuery() function takes parameters from the calling function. Thus, changes to the parameters of the calling function do not require changes to the function definition.

    Native queries can be created on entities used by the application. If in the previous version the entity only contained properties, then in version 2.0, the entity can also contain functions for native queries. However, entities in versions 1 and 2 both support functions but functions with native queries are only supported in version 2.0.

    +

    Pagination and Sorting

    +

    In MagicObject version 2.7, support for pageable and sortable functionality has been added to native queries. Previously, native queries did not support pagination and sorting directly. Instead, users had to manually include SORT BY and LIMIT OFFSET clauses in their queries, which made them less flexible. This approach was problematic because each Database Management System (DBMS) has its own syntax for writing queries, making it cumbersome to adapt queries for different platforms.

    +

    With the introduction of pageable and sortable support in version 2.7, users can now easily pass pagination parameters using the PicoPageable type and sorting parameters using the PicoSortable type directly into their native queries. These parameters can be placed anywhere within the query, but it is recommended to position them either at the beginning or the end of the query for optimal readability and organization.

    +

    This enhancement makes native queries more flexible and easier to maintain, as the logic for pagination and sorting is handled automatically, without requiring manual intervention for each DBMS. As a result, users can now write cleaner, more efficient, and database-agnostic native queries.

    Debug Query

    MagicObject checks if the database connection has a debugging function for queries. If available, it sends the executed query along with the parameter values to this function, aiding users in identifying errors during query definition and execution.

    Example:

    @@ -8827,6 +8878,28 @@

    Debug Query

    // Call parent method to execute the query return $this->executeNativeQuery(); } + + /** + * Native query 13 + * + * This method will return a prepared statement for further operations if necessary. + * + * @param PicoPagebale $pageable + * @param PicoSortable $sortable + * @param bool $aktif The active status to filter results. + * @return MagicObject[] + * @query(" + SELECT supervisor.* + FROM supervisor + WHERE supervisor.supervisor_id in :supervisorId + AND supervisor.aktif = :aktif + ") + */ + public function native13($pageable, $sortable, $aktif) + { + // Call parent method to execute the query + return $this->executeNativeQuery(); + } } $obj = new Supervisor(null, $database); @@ -8879,7 +8952,25 @@

    Debug Query

    echo "Alamat: " . $native8->getTelepon() . "\r\n"; echo "Alamat: " . $native9[0]->getTelepon() . "\r\n"; echo "Alamat: " . $native10->getTelepon() . "\r\n"; -echo "Alamat: " . $native11[0]->getTelepon() . "\r\n"; +echo "Alamat: " . $native11[0]->getTelepon() . "\r\n"; + +$sortable = new PicoSortable(); +$sortable->addSortable(new PicoSort("nama", PicoSort::ORDER_TYPE_ASC)); +$pageable = new PicoPageable(new PicoPage(3, 20)); + +try +{ + $native13 = $obj->native13($pageable, $sortable, true); + echo "\r\nnative13:\r\n"; + foreach($native13 as $sup) + { + echo $sup."\r\n\r\n"; + } +} +catch(Exception $e) +{ + echo $e->getMessage(); +}

    For the purpose of exporting large amounts of data, use the PDOStatement return type. PDOStatement allows users to read one by one and process it immediately, allowing PHP to release memory from the previous process. PHP does not need to store very large data in a variable.

    Example 12 shows how to use array parameters.

    For example:

    @@ -9130,7 +9221,7 @@

    Best Practices

    By leveraging the native query feature in MagicObject, you can create efficient and maintainable database interactions, enhancing your application's performance and security.

    -
    +

    Multiple Database Connections

    MagicObject version 2 introduces support for multiple database connections, enabling users to manage entities stored across different databases seamlessly. When performing operations such as JOINs with entities from multiple databases, it is essential to define a database connection for each entity involved.

    Example Scenario

    @@ -9295,7 +9386,7 @@

    Conclusion

    With MagicObject version 2, managing entities across multiple database connections is straightforward. By defining the correct associations and utilizing the provided methods, users can effectively work with complex data structures that span multiple databases. Make sure to handle exceptions properly to ensure robustness in your application.

    -
    +

    Dump Database

    We can dump database to another database type. We do not need any database converter. Just define the target database type when we dump the database.

    Database Dump Overview

    @@ -9809,7 +9900,7 @@

    Summary

    This approach allows developers to quickly switch between database types and manage their database schemas and data efficiently. The use of dedicated instances of PicoDatabaseDump for multiple tables ensures clarity and organization in your database operations.

    -
    +

    Object Label

    <?php
     
    @@ -9934,7 +10025,7 @@ 

    Object Label

    // it will print "Admin Create"
    -
    +

    Database Query Builder

    Database Query Builder is a feature for creating object-based database queries. The output of the Database Query Builder is a query that can be directly executed by the database used.

    Database Query Builder is actually designed for all relational databases but is currently only available in two languages, namely MySQL and PostgreSQL. MagicObject internally uses the Database Query Builder to create queries based on given methods and parameters.

    @@ -10201,7 +10292,7 @@

    Methods

    This way, $active will be escaped before being executed by the database. You don't need to escape it first.

    -
    +

    PicoSqlite

    Overview

    PicoSqlite is a PHP class designed for simplified interactions with SQLite databases using PDO (PHP Data Objects). This class extends PicoDatabase and provides methods for connecting to the database, creating tables, and performing basic CRUD (Create, Read, Update, Delete) operations.

    @@ -10568,8 +10659,8 @@

    Conclusion

    PicoSqlite provides an efficient way to interact with SQLite databases. Its straightforward API allows developers to perform common database operations with minimal code. For more advanced database operations, consider extending the class or using additional PDO features.

    -
    -

    Upload File

    +
    +

    File Upload

    Overview

    Uploading files can be challenging, especially for novice developers. This guide explains how to manage single and multiple file uploads in PHP, highlighting the differences and providing straightforward examples.

    Key Features

    @@ -10635,7 +10726,76 @@

    Summary

    This implementation offers a straightforward way to manage file uploads in PHP, abstracting complexities for developers. By using methods like getAll() and isMultiple(), developers can seamlessly handle both types of uploads without needing to write separate logic for each scenario. This approach not only improves code maintainability but also enhances the developer experience.

    -
    +
    +

    Resumable File Download

    +

    Namespace

    +

    MagicObject\File

    +

    Description

    +

    The PicoDownloadFile class is designed to facilitate efficient file downloading in PHP, supporting partial content (range requests) for large files. It ensures that requested files exist, handles errors gracefully, and enables downloading in chunks to minimize server load and bandwidth consumption, particularly for large files.

    +

    The class supports the following:

    +
      +
    • Verifying the existence of the file.
    • +
    • Handling byte-range requests for resuming downloads.
    • +
    • Sending appropriate HTTP headers to manage the download.
    • +
    • Streaming the file to the client in manageable chunks (default size: 8 KB).
    • +
    • Returning relevant HTTP status codes and error messages.
    • +
    +

    This class is ideal for scenarios where large files need to be served to clients and you want to offer functionality like resuming interrupted downloads.

    +

    Constructor

    +
    __construct($filepath, $filename = null)
    +

    Parameters:

    +
      +
    • $filepath (string): The full path to the file that should be downloaded.
    • +
    • $filename (string|null, optional): The name of the file for download. If not provided, the filename is extracted from the filepath using basename().
    • +
    +

    Description: Initializes the PicoDownloadFile object with the path of the file to be downloaded and an optional filename for the download response. If the filename is not specified, the base name of the file is used.

    +

    Example:

    +
    $file = new PicoDownloadFile("/path/to/large-file.zip", "downloaded-file.zip");
    +

    Method

    +
    download($exit = false)
    +

    Parameters:

    +
      +
    • $exit (bool, optional): Whether to terminate the script after sending the file. Default is false.
    • +
    +

    Returns:

    +
      +
    • bool: Returns true if the entire file was successfully sent, false if only part of the file was sent (due to range requests).
    • +
    +

    Description: This method is responsible for initiating the file download process. It performs the following:

    +
      +
    1. Verifies the existence of the file.
    2. +
    3. Handles byte-range requests for partial downloads (useful for resuming interrupted downloads).
    4. +
    5. Sends the appropriate HTTP headers for the file download.
    6. +
    7. Streams the file to the client in chunks of 8 KB (by default).
    8. +
    +

    If $exit is set to true, the script will terminate after the file is sent.

    +

    Example 1

    +
    <?php
    +require 'vendor/autoload.php'; // Include the PicoDownloadFile class
    +$path = "/path/to/large-file.zip";
    +$localName = "downloaded-file.zip";
    +$file = new PicoDownloadFile($path, $localName);
    +$file->download(true); // Initiate download and terminate the script after sending
    +

    Example 2

    +
    <?php
    +require 'vendor/autoload.php'; // Include the PicoDownloadFile class
    +$path = "/path/to/large-file.zip";
    +$localName = "downloaded-file.zip";
    +$file = new PicoDownloadFile($path, $localName);
    +$finished = $file->download(false); // Initiate download without terminate the script after sending
    +if($finished && file_exists($path))
    +{
    +    unlink($path); // Delete file when finish
    +}
    +

    Error Handling

    +
      +
    • 404 - File Not Found: If the file does not exist at the specified path, a 404 error is returned.
    • +
    • 416 - Range Not Satisfiable: If an invalid byte range is requested (e.g., the start byte is larger than the end byte), a 416 error is returned.
    • +
    • 500 - Internal Server Error: If there is an issue opening the file for reading (e.g., permissions issues), a 500 error is returned.
    • +
    +
    + +

    Language

    MagicObject supports multilingual applications. MagicObject allows developers to create entities that support a wide variety of languages that users can choose from. At the same time, different users can use different languages.

    To create table with multiple language, create new class from DataTable object. We can copy data from aother object to DataTable easly.

    @@ -10855,7 +11015,7 @@

    Language

    echo $apa;
    -
    +

    Database Migration

    MagicObject allows users to import data from a database with different table names and column names between the source database and the destination database. This feature is used by developers who develop applications that are already used in production environments.

    On the one hand, the application requires a new database structure according to what is defined by the developer. On the other hand, users want to use existing data.

    diff --git a/manual/index.html.md b/manual/index.html.md index c4528002..ab8a49fe 100644 --- a/manual/index.html.md +++ b/manual/index.html.md @@ -26,6 +26,7 @@ includes: - input - session - database + - with-pdo - entity - specification - pagable @@ -37,6 +38,7 @@ includes: - database-query-builder - sqlite - upload-file + - download-file - data-table - database-migration --- diff --git a/src/DataLabel/PicoDataLabels.php b/src/DataLabel/PicoDataLabels.php index 6941f728..bedac271 100644 --- a/src/DataLabel/PicoDataLabels.php +++ b/src/DataLabel/PicoDataLabels.php @@ -41,7 +41,8 @@ public function append($data) */ public function generate() { - foreach ($this->data as $data) { + foreach ($this->data as $data) //NOSONAR + { // Implementation for processing each data label goes here } } diff --git a/src/Database/PicoDatabase.php b/src/Database/PicoDatabase.php index 24f66217..250c0e9b 100644 --- a/src/Database/PicoDatabase.php +++ b/src/Database/PicoDatabase.php @@ -94,6 +94,198 @@ class PicoDatabase //NOSONAR */ protected $callbackDebugQuery = null; + /** + * Creates a PicoDatabase instance from an existing PDO connection. + * + * This static method accepts a PDO connection object, initializes a new + * PicoDatabase instance, and sets up the database connection and type. + * It also marks the database as connected and returns the configured + * PicoDatabase object. + * + * @param PDO $pdo The PDO connection object representing an active connection to the database. + * @return PicoDatabase Returns a new instance of the PicoDatabase class, + * with the PDO connection and database type set. + */ + public static function fromPdo($pdo) + { + $database = new self(new SecretObject()); + $database->databaseConnection = $pdo; + $database->databaseType = $database->getDbType($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)); + $database->connected = true; + $database->databaseCredentials = $database->getDatabaseCredentialsFromPdo($pdo); + return $database; + } + + /** + * Get PDO connection details, including driver, host, port, database name, schema, and time zone. + * + * This function retrieves information about the PDO connection, such as the database driver, host, port, + * database name, schema, and time zone based on the type of database (e.g., MySQL, PostgreSQL, SQLite). + * + * It uses the PDO connection's attributes and queries the database if necessary to obtain the schema name and time zone. + * + * @param PDO $pdo The PDO connection object. + * @return SecretObject Returns a SecretObject containing the connection details (driver, host, port, database name, schema, and time zone). + * + * @throws PDOException If there is an error with the PDO query or connection. + */ + private function getDatabaseCredentialsFromPdo($pdo) + { + // Get the driver name (e.g., mysql, pgsql, sqlite) + $driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + + // Get the connection status, which includes the DSN (Data Source Name) + $dsn = $pdo->getAttribute(PDO::ATTR_CONNECTION_STATUS); + $dsnParts = parse_url($dsn); + + // Extract the host from the DSN (if available) + $host = isset($dsnParts['host']) ? $dsnParts['host'] : null; + + // Extract the port from the DSN (if available) + $port = isset($dsnParts['port']) ? $dsnParts['port'] : null; + + // Get the database name from the DSN (usually found at the end of the DSN after host and port) + $databaseName = isset($dsnParts['path']) ? ltrim($dsnParts['path'], '/') : null; + + // Initialize the schema and time zone + $schema = null; + $timezone = null; + + // Determine the database type + $dbType = $this->getDbType($driver); + + // Retrieve the schema and time zone based on the database type + if ($dbType == PicoDatabaseType::DATABASE_TYPE_PGSQL) { + // For PostgreSQL, fetch the current schema and time zone using queries + $stmt = $pdo->query('SELECT current_schema()'); + $schema = $stmt->fetchColumn(); // Fetch the schema name + + $stmtTimezone = $pdo->query('SHOW timezone'); + $timezone = $stmtTimezone->fetchColumn(); // Fetch the time zone + } + elseif ($dbType == PicoDatabaseType::DATABASE_TYPE_MYSQL || $dbType == PicoDatabaseType::DATABASE_TYPE_MARIADB) { + // For MySQL, the schema is the same as the database name + $schema = $databaseName; // MySQL schema is the database name + + // Retrieve the global time zone from MySQL + $stmtTimezone = $pdo->query('SELECT @@global.time_zone'); + $timezone = $stmtTimezone->fetchColumn(); // Fetch the global time zone + + // If the time zone is set to 'SYSTEM', retrieve the system's time zone and convert it + if ($timezone == 'SYSTEM') { + $stmtSystemTimeZone = $pdo->query('SELECT @@system_time_zone'); + $systemTimeZone = $stmtSystemTimeZone->fetchColumn(); + + // Convert MySQL system time zone to PHP-compatible time zone (e.g., 'Asia/Jakarta') + // This conversion may require a lookup table, as MySQL system time zones + // (e.g., 'CST', 'PST') are not directly equivalent to PHP time zones (e.g., 'Asia/Jakarta'). + // Here, we will simply return the system time zone as a placeholder: + $timezone = $systemTimeZone; + } + } + else { + // For other drivers, set schema and time zone to null (or handle it as needed) + $schema = null; + $timezone = null; + } + + // If the time zone is provided, convert it to a recognized PHP time zone if necessary + if (isset($timezone)) { + $timezone = $this->mysqlToPhpTimezone($timezone); + } + + // Create and populate the SecretObject with the connection details + $databaseCredentials = new SecretObject(); + $databaseCredentials->setDriver($driver); + $databaseCredentials->setHost($host); + $databaseCredentials->setPort($port); + $databaseCredentials->setDatabaseName($databaseName); + $databaseCredentials->setDatabaseSchema($schema); + $databaseCredentials->setTimeZone($timezone); + + // Return the populated SecretObject containing the connection details + return $databaseCredentials; + } + + /** + * Map MySQL system time zone or abbreviations like 'WIB' to a valid PHP time zone. + * + * This function converts time zone abbreviations (like 'WIB', 'WITA', 'WIT') or system time zones + * to a recognized PHP time zone format (e.g., 'Asia/Jakarta'). + * + * @param string $timezoneAbbr The time zone abbreviation or system time zone (e.g., 'WIB', 'SYSTEM'). + * @return string|null Returns a PHP-compatible time zone (e.g., 'Asia/Jakarta') or null if not recognized. + */ + private function mysqlToPhpTimezone($timezoneAbbr) + { + $timezoneMapping = [ + // Indonesia + 'WIB' => 'Asia/Jakarta', // Western Indonesia Time (e.g., Jakarta, Bali) + 'WITA' => 'Asia/Makassar', // Central Indonesia Time (e.g., Bali, Sulawesi) + 'WIT' => 'Asia/Jayapura', // Eastern Indonesia Time (e.g., Papua) + + // Common USA Time Zones + 'PST' => 'America/Los_Angeles', // Pacific Standard Time (Standard Time) + 'PDT' => 'America/Los_Angeles', // Pacific Daylight Time (Daylight Saving Time) + 'MST' => 'America/Denver', // Mountain Standard Time + 'MDT' => 'America/Denver', // Mountain Daylight Time + 'CST' => 'America/Chicago', // Central Standard Time + 'CDT' => 'America/Chicago', // Central Daylight Time + 'EST' => 'America/New_York', // Eastern Standard Time + 'EDT' => 'America/New_York', // Eastern Daylight Time + 'AKST' => 'America/Anchorage', // Alaska Standard Time + 'AKDT' => 'America/Anchorage', // Alaska Daylight Time + 'HST' => 'Pacific/Honolulu', // Hawaii Standard Time + + // United Kingdom + 'GMT' => 'Europe/London', // Greenwich Mean Time (Standard Time) + 'BST' => 'Europe/London', // British Summer Time (Daylight Saving Time) + + // Central Europe + 'CET' => 'Europe/Paris', // Central European Time + 'CEST' => 'Europe/Paris', // Central European Summer Time (Daylight Saving Time) + + // Central Asia and Russia + 'MSK' => 'Europe/Moscow', // Moscow Standard Time + 'MSD' => 'Europe/Moscow', // Moscow Daylight Time (not used anymore) + + // Australia + 'AEST' => 'Australia/Sydney', // Australian Eastern Standard Time + 'AEDT' => 'Australia/Sydney', // Australian Eastern Daylight Time + 'ACST' => 'Australia/Adelaide', // Australian Central Standard Time + 'ACDT' => 'Australia/Adelaide', // Australian Central Daylight Time + 'AWST' => 'Australia/Perth', // Australian Western Standard Time + + // Africa + 'CAT' => 'Africa/Harare', // Central Africa Time + 'EAT' => 'Africa/Nairobi', // East Africa Time + 'WAT' => 'Africa/Algiers', // West Africa Time + + // India + 'IST' => 'Asia/Kolkata', // Indian Standard Time + + // China and East Asia + 'CST' => 'Asia/Shanghai', // China Standard Time + 'JST' => 'Asia/Tokyo', // Japan Standard Time + 'KST' => 'Asia/Seoul', // Korea Standard Time + + // Other time zones + 'UTC' => 'UTC', // Coordinated Universal Time + 'Z' => 'UTC', // Zulu time (same as UTC) + 'ART' => 'Africa/Argentina', // Argentina Time + 'NFT' => 'Pacific/Norfolk', // Norfolk Time Zone (Australia) + + // Time zones used in specific areas + 'NST' => 'Asia/Kolkata', // Newfoundland Standard Time (if used as an abbreviation) + ]; + + // Return the mapped PHP time zone or null if not found + return isset($timezoneMapping[$timezoneAbbr]) ? $timezoneMapping[$timezoneAbbr] : null; + } + + + + /** * Constructor to initialize the PicoDatabase object. * @@ -154,7 +346,7 @@ private function connectSqlite() $path = $this->databaseCredentials->getDatabaseFilePath(); if(!isset($path) || empty($path)) { - throw new InvalidDatabaseConfiguration("Database path may not be empty. Please check your database configuration!"); + throw new InvalidDatabaseConfiguration("Database path may not be empty. Please check your database configuration on {database_file_path}!"); } try { $this->databaseConnection = new PDO("sqlite:" . $path); @@ -189,7 +381,7 @@ private function connectRDMS($withDatabase = true) throw new InvalidDatabaseConfiguration("Database username may not be empty. Please check your database configuration!"); } $initialQueries = "SET time_zone = '$timeZoneOffset';"; - if ($this->getDatabaseType() == PicoDatabaseType::DATABASE_TYPE_POSTGRESQL && + if ($this->getDatabaseType() == PicoDatabaseType::DATABASE_TYPE_PGSQL && $this->databaseCredentials->getDatabaseSchema() != null && $this->databaseCredentials->getDatabaseSchema() != "") { $initialQueries .= "SET search_path TO " . $this->databaseCredentials->getDatabaseSchema(); @@ -215,11 +407,18 @@ private function connectRDMS($withDatabase = true) /** * Determine the database type based on the provided database type string. * - * This method checks the input string for common database type identifiers (SQLite, PostgreSQL, - * MariaDB, MySQL) and returns the corresponding constant from the PicoDatabaseType class. + * This method evaluates the given string to identify common database type names + * (e.g., SQLite, PostgreSQL, MariaDB, MySQL) and returns the corresponding + * constant from the `PicoDatabaseType` class that represents the type of database. + * The function performs case-insensitive string matching using `stripos` to check for + * keywords like "sqlite", "postgre", "pgsql", "maria", and defaults to MySQL if no match is found. * - * @param string $databaseType The database type string to evaluate. - * @return string The corresponding database type constant from PicoDatabaseType. + * @param string $databaseType The database type string to evaluate, such as 'SQLite', 'PostgreSQL', 'MariaDB', or 'MySQL'. + * @return string The corresponding database type constant from `PicoDatabaseType`: + * - `PicoDatabaseType::DATABASE_TYPE_SQLITE` + * - `PicoDatabaseType::DATABASE_TYPE_PGSQL` + * - `PicoDatabaseType::DATABASE_TYPE_MARIADB` + * - `PicoDatabaseType::DATABASE_TYPE_MYSQL` */ private function getDbType($databaseType) // NOSONAR { @@ -229,7 +428,7 @@ private function getDbType($databaseType) // NOSONAR } else if(stripos($databaseType, 'postgre') !== false || stripos($databaseType, 'pgsql') !== false) { - return PicoDatabaseType::DATABASE_TYPE_POSTGRESQL; + return PicoDatabaseType::DATABASE_TYPE_PGSQL; } else if(stripos($databaseType, 'maria') !== false) { @@ -241,6 +440,32 @@ private function getDbType($databaseType) // NOSONAR } } + /** + * Determines the database driver based on the provided database type. + * + * This function takes a string representing the database type and returns + * the corresponding database driver constant from the `PicoDatabaseType` class. + * It supports SQLite, PostgreSQL, and MySQL/MariaDB types. + * + * @param string $databaseType The type of the database (e.g., 'sqlite', 'postgres', 'pgsql', 'mysql', 'mariadb'). + * + * @return string The corresponding database driver constant, one of: + * - `sqlite` + * - `pgsql` + * - `mysql` + */ + private function getDbDriver($databaseType) + { + if (stripos($databaseType, 'sqlite') !== false) { + return PicoDatabaseType::DATABASE_TYPE_SQLITE; + } else if (stripos($databaseType, 'postgre') !== false || stripos($databaseType, 'pgsql') !== false) { + return PicoDatabaseType::DATABASE_TYPE_PGSQL; + } else { + return PicoDatabaseType::DATABASE_TYPE_MYSQL; + } + } + + /** * Create a connection string. * @@ -265,18 +490,20 @@ private function constructConnectionString($withDatabase = true) $emptyValue .= $emptyName ? "{database_name}" : ""; throw new InvalidDatabaseConfiguration("Invalid database configuration. $emptyValue. Please check your database configuration!"); } - return $this->databaseCredentials->getDriver() . ':host=' . $this->databaseCredentials->getHost() . '; port=' . ((int) $this->databaseCredentials->getPort()) . '; dbname=' . $this->databaseCredentials->getDatabaseName(); + return $this->getDbDriver($this->databaseCredentials->getDriver()) . ':host=' . $this->databaseCredentials->getHost() . '; port=' . ((int) $this->databaseCredentials->getPort()) . '; dbname=' . $this->databaseCredentials->getDatabaseName(); } else { if ($invalidParam1) { throw new InvalidDatabaseConfiguration("Invalid database configuration. $emptyValue. Please check your database configuration!"); } - return $this->databaseCredentials->getDriver() . ':host=' . $this->databaseCredentials->getHost() . '; port=' . ((int) $this->databaseCredentials->getPort()); + return $this->getDbDriver($this->databaseCredentials->getDriver()) . ':host=' . $this->databaseCredentials->getHost() . '; port=' . ((int) $this->databaseCredentials->getPort()); } } /** * Disconnect from the database. * + * This method sets the database connection to `null`, effectively closing the connection to the database. + * * @return self Returns the current instance for method chaining. */ public function disconnect() @@ -286,9 +513,11 @@ public function disconnect() } /** - * Set the time zone offset. + * Set the time zone offset for the database session. + * + * This method sets the time zone offset for the current session, which can be useful for time-related operations. * - * @param string $timeZoneOffset Client time zone. + * @param string $timeZoneOffset The time zone offset to set for the session (e.g., '+00:00', 'Europe/London'). * @return self Returns the current instance for method chaining. */ public function setTimeZoneOffset($timeZoneOffset) @@ -299,9 +528,11 @@ public function setTimeZoneOffset($timeZoneOffset) } /** - * Change the database. + * Switch to a different database. * - * @param string $databaseName Database name. + * This method changes the currently active database to the specified one. + * + * @param string $databaseName The name of the database to switch to. * @return self Returns the current instance for method chaining. */ public function useDatabase($databaseName) @@ -312,10 +543,13 @@ public function useDatabase($databaseName) } /** - * Set autocommit ON or OFF. + * Set autocommit mode for transactions. + * + * This method enables or disables autocommit mode for database transactions. When autocommit is off, + * you must explicitly call `commit()` or `rollback()` to finalize or revert the transaction. * - * @param bool $autocommit Flag autocommit. - * @return bool True if autocommit is set successfully, false otherwise. + * @param bool $autocommit Flag indicating whether autocommit should be enabled (`true`) or disabled (`false`). + * @return bool Returns `true` if the autocommit setting was successfully updated, `false` otherwise. */ public function setAudoCommit($autocommit) { @@ -324,9 +558,11 @@ public function setAudoCommit($autocommit) } /** - * Commit the transaction. + * Commit the current transaction. + * + * This method commits the transaction, making all changes made during the transaction permanent. * - * @return bool True if the transaction was committed successfully, false otherwise. + * @return bool Returns `true` if the transaction was successfully committed, `false` otherwise. */ public function commit() { @@ -334,9 +570,11 @@ public function commit() } /** - * Rollback the transaction. + * Rollback the current transaction. * - * @return bool True if the transaction was rolled back successfully, false otherwise. + * This method rolls back the transaction, undoing any changes made during the transaction. + * + * @return bool Returns `true` if the transaction was successfully rolled back, `false` otherwise. */ public function rollback() { @@ -344,9 +582,11 @@ public function rollback() } /** - * Get the database connection. + * Get the current database connection. + * + * This method returns the active PDO connection object, which can be used for executing queries directly. * - * @return PDO Represents a connection between PHP and a database server. + * @return PDO The active PDO connection object representing the connection to the database server. */ public function getDatabaseConnection() { @@ -354,11 +594,14 @@ public function getDatabaseConnection() } /** - * Execute a query. + * Execute a SQL query. + * + * This method executes a SQL query with optional parameters and returns the resulting PDO statement object. * - * @param string $sql SQL to be executed. - * @param array|null $params Optional parameters for the SQL query. - * @return PDOStatement|false Returns the PDOStatement object if successful, or false on failure. + * @param string $sql The SQL query to execute. + * @param array|null $params Optional parameters to bind to the query. + * @return PDOStatement|false Returns a `PDOStatement` object if the query was executed successfully, + * or `false` if the execution failed. * @throws PDOException If an error occurs while executing the query. */ public function query($sql, $params = null) @@ -367,13 +610,15 @@ public function query($sql, $params = null) } /** - * Fetch a result. + * Fetch a result from the database. * - * @param string $sql SQL to be executed. - * @param int $tentativeType Tentative type for fetch mode (e.g., PDO::FETCH_ASSOC). - * @param mixed $defaultValue Default value to return if no results found. - * @param array|null $params Optional parameters for the SQL query. - * @return array|object|stdClass|null Returns the fetched result as an array, object, or stdClass, or the default value if no results are found. + * This method executes a query and returns a single result. If no result is found, the default value is returned. + * + * @param string $sql SQL query to be executed. + * @param int $tentativeType The fetch mode to be used (e.g., PDO::FETCH_ASSOC). + * @param mixed $defaultValue The default value to return if no results are found. + * @param array|null $params Optional parameters to bind to the SQL query. + * @return array|object|stdClass|null Returns the fetched result (array, object, or stdClass), or the default value if no results are found. */ public function fetch($sql, $tentativeType = PDO::FETCH_ASSOC, $defaultValue = null, $params = null) { @@ -407,11 +652,13 @@ public function fetch($sql, $tentativeType = PDO::FETCH_ASSOC, $defaultValue = n } /** - * Check if a record exists. + * Check if a record exists in the database. + * + * This method executes a query and checks if any record is returned. * - * @param string $sql SQL to be executed. - * @param array|null $params Optional parameters for the SQL query. - * @return bool True if the record exists, false otherwise. + * @param string $sql SQL query to be executed. + * @param array|null $params Optional parameters to bind to the SQL query. + * @return bool Returns `true` if the record exists, `false` otherwise. * @throws NullPointerException If the database connection is null. */ public function isRecordExists($sql, $params = null) @@ -440,12 +687,14 @@ public function isRecordExists($sql, $params = null) } /** - * Fetch all results. + * Fetch all results from the database. + * + * This method executes a query and returns all matching results. If no results are found, the default value is returned. * - * @param string $sql SQL to be executed. - * @param int $tentativeType Tentative type for fetch mode (e.g., PDO::FETCH_ASSOC). - * @param mixed $defaultValue Default value to return if no results found. - * @param array|null $params Optional parameters for the SQL query. + * @param string $sql SQL query to be executed. + * @param int $tentativeType The fetch mode to be used (e.g., PDO::FETCH_ASSOC). + * @param mixed $defaultValue The default value to return if no results are found. + * @param array|null $params Optional parameters to bind to the SQL query. * @return array|null Returns an array of results or the default value if no results are found. */ public function fetchAll($sql, $tentativeType = PDO::FETCH_ASSOC, $defaultValue = null, $params = null) @@ -480,10 +729,12 @@ public function fetchAll($sql, $tentativeType = PDO::FETCH_ASSOC, $defaultValue } /** - * Execute a query without returning anything. + * Execute a SQL query without returning any results. * - * @param string $sql Query string to be executed. - * @param array|null $params Optional parameters for the SQL query. + * This method executes a query without expecting any result, typically used for non-SELECT queries (INSERT, UPDATE, DELETE). + * + * @param string $sql SQL query to be executed. + * @param array|null $params Optional parameters to bind to the SQL query. * @throws NullPointerException If the database connection is null. */ public function execute($sql, $params = null) @@ -503,11 +754,13 @@ public function execute($sql, $params = null) } /** - * Execute a query and return the statement. + * Execute a SQL query and return the statement object. + * + * This method executes a query and returns the PDOStatement object, which can be used to fetch results or retrieve row count. * - * @param string $sql Query string to be executed. - * @param array|null $params Optional parameters for the SQL query. - * @return PDOStatement|false Returns the PDOStatement object if successful, or false on failure. + * @param string $sql SQL query to be executed. + * @param array|null $params Optional parameters to bind to the SQL query. + * @return PDOStatement|false Returns the PDOStatement object if successful, or `false` on failure. * @throws NullPointerException If the database connection is null. * @throws PDOException If an error occurs while executing the query. */ @@ -530,11 +783,13 @@ public function executeQuery($sql, $params = null) } /** - * Execute an insert query. + * Execute an insert query and return the statement. * - * @param string $sql Query string to be executed. - * @param array|null $params Optional parameters for the SQL query. - * @return PDOStatement|false Returns the PDOStatement object if successful, or false on failure. + * This method executes an insert query and returns the PDOStatement object. + * + * @param string $sql SQL query to be executed. + * @param array|null $params Optional parameters to bind to the SQL query. + * @return PDOStatement|false Returns the PDOStatement object if successful, or `false` on failure. */ public function executeInsert($sql, $params = null) { @@ -544,11 +799,13 @@ public function executeInsert($sql, $params = null) } /** - * Execute an update query. + * Execute an update query and return the statement. + * + * This method executes an update query and returns the PDOStatement object. * - * @param string $sql Query string to be executed. - * @param array|null $params Optional parameters for the SQL query. - * @return PDOStatement|false Returns the PDOStatement object if successful, or false on failure. + * @param string $sql SQL query to be executed. + * @param array|null $params Optional parameters to bind to the SQL query. + * @return PDOStatement|false Returns the PDOStatement object if successful, or `false` on failure. */ public function executeUpdate($sql, $params = null) { @@ -558,11 +815,13 @@ public function executeUpdate($sql, $params = null) } /** - * Execute a delete query. + * Execute a delete query and return the statement. + * + * This method executes a delete query and returns the PDOStatement object. * - * @param string $sql Query string to be executed. - * @param array|null $params Optional parameters for the SQL query. - * @return PDOStatement|false Returns the PDOStatement object if successful, or false on failure. + * @param string $sql SQL query to be executed. + * @param array|null $params Optional parameters to bind to the SQL query. + * @return PDOStatement|false Returns the PDOStatement object if successful, or `false` on failure. */ public function executeDelete($sql, $params = null) { @@ -572,11 +831,13 @@ public function executeDelete($sql, $params = null) } /** - * Execute a transaction query. + * Execute a transaction query and return the statement. * - * @param string $sql Query string to be executed. - * @param array|null $params Optional parameters for the SQL query. - * @return PDOStatement|false Returns the PDOStatement object if successful, or false on failure. + * This method executes a query as part of a transaction and returns the PDOStatement object. + * + * @param string $sql SQL query to be executed. + * @param array|null $params Optional parameters to bind to the SQL query. + * @return PDOStatement|false Returns the PDOStatement object if successful, or `false` on failure. */ public function executeTransaction($sql, $params = null) { @@ -586,11 +847,13 @@ public function executeTransaction($sql, $params = null) } /** - * Execute a callback query function. + * Execute a callback query function after executing the query. + * + * This method calls the provided callback function after executing a query. * - * @param string $query SQL to be executed. - * @param array|null $params Optional parameters for the SQL query. - * @param string|null $type Query type. + * @param string $query SQL query to be executed. + * @param array|null $params Optional parameters to bind to the SQL query. + * @param string|null $type Type of the query (e.g., INSERT, UPDATE, DELETE, etc.). */ private function executeCallback($query, $params = null, $type = null) { @@ -614,8 +877,10 @@ private function executeCallback($query, $params = null, $type = null) /** * Execute a debug query function. * - * @param string $query SQL to be executed. - * @param array|null $params Optional parameters for the SQL query. + * This method calls a debug callback function if it is set. + * + * @param string $query SQL query to be executed. + * @param array|null $params Optional parameters to bind to the SQL query. */ private function executeDebug($query, $params = null) { @@ -641,7 +906,11 @@ private function executeDebug($query, $params = null) /** * Generate a unique 20-byte ID. * - * @return string 20 bytes unique identifier. + * This method generates a unique ID by concatenating a 13-character string + * from `uniqid()` with a 6-character random hexadecimal string, ensuring + * the resulting string is 20 characters in length. + * + * @return string A unique 20-byte identifier. */ public function generateNewId() { @@ -656,7 +925,11 @@ public function generateNewId() /** * Get the last inserted ID. * - * @param string|null $name Sequence name (e.g., PostgreSQL). + * This method retrieves the ID of the last inserted record. Optionally, + * you can provide a sequence name (e.g., for PostgreSQL) to fetch the last + * inserted ID from a specific sequence. + * + * @param string|null $name The sequence name (e.g., PostgreSQL). Default is null. * @return string|false Returns the last inserted ID as a string, or false if there was an error. */ public function lastInsertId($name = null) @@ -665,9 +938,12 @@ public function lastInsertId($name = null) } /** - * Get the value of databaseCredentials. + * Get the value of database credentials. + * + * This method returns the object containing the database credentials used + * to establish the database connection. * - * @return SecretObject Returns the database credentials object. + * @return SecretObject The database credentials object. */ public function getDatabaseCredentials() { @@ -675,7 +951,10 @@ public function getDatabaseCredentials() } /** - * Get indication whether the database is connected or not. + * Check whether the database is connected. + * + * This method returns a boolean value indicating whether the database + * connection is currently active. * * @return bool Returns true if connected, false otherwise. */ @@ -685,9 +964,16 @@ public function isConnected() } /** - * Get the database type. + * Get the type of the database. + * + * This method returns the type of the database that is currently connected. + * The possible values are constants from the `PicoDatabaseType` class: + * - `PicoDatabaseType::DATABASE_TYPE_MYSQL` + * - `PicoDatabaseType::DATABASE_TYPE_MARIADB` + * - `PicoDatabaseType::DATABASE_TYPE_PGSQL` + * - `PicoDatabaseType::DATABASE_TYPE_SQLITE` * - * @return string Returns the type of the database (e.g., MySQL, PostgreSQL). + * @return string The type of the database. */ public function getDatabaseType() { diff --git a/src/Database/PicoDatabaseCredentials.php b/src/Database/PicoDatabaseCredentials.php index beb82957..03fd6219 100644 --- a/src/Database/PicoDatabaseCredentials.php +++ b/src/Database/PicoDatabaseCredentials.php @@ -31,7 +31,7 @@ class PicoDatabaseCredentials extends SecretObject { /** - * Database driver (e.g., 'mysql', 'postgresql', 'mariadb). + * Database driver (e.g., 'mysql', 'pgsql', 'mariadb', 'sqlite'). * * @var string */ diff --git a/src/Database/PicoDatabaseQueryBuilder.php b/src/Database/PicoDatabaseQueryBuilder.php index 5398e3c2..2a966dbd 100644 --- a/src/Database/PicoDatabaseQueryBuilder.php +++ b/src/Database/PicoDatabaseQueryBuilder.php @@ -98,7 +98,7 @@ public function isMySql() */ public function isPgSql() { - return strcasecmp($this->databaseType, PicoDatabaseType::DATABASE_TYPE_POSTGRESQL) == 0; + return strcasecmp($this->databaseType, PicoDatabaseType::DATABASE_TYPE_PGSQL) == 0; } /** @@ -642,7 +642,7 @@ public function escapeSQL($query) stripos($this->databaseType, PicoDatabaseType::DATABASE_TYPE_SQLITE) !== false) { return str_replace(["\r", "\n"], ["\\r", "\\n"], addslashes($query)); } - if (stripos($this->databaseType, PicoDatabaseType::DATABASE_TYPE_POSTGRESQL) !== false) { + if (stripos($this->databaseType, PicoDatabaseType::DATABASE_TYPE_PGSQL) !== false) { return str_replace(["\r", "\n"], ["\\r", "\\n"], $this->replaceQuote($query)); } return $query; @@ -844,6 +844,62 @@ public function addQueryParameters($query) return $buffer; } + /** + * Adds pagination and sorting clauses to a native query string. + * + * This function appends the appropriate `ORDER BY` and `LIMIT $limit OFFSET $offset` or `LIMIT $offset, $limit` + * clauses to the provided SQL query string based on the given pagination and sorting parameters. + * It supports various database management systems (DBMS) and adjusts the query syntax + * accordingly (e.g., for PostgreSQL, SQLite, MySQL, MariaDB, etc.). + * + * @param string $queryString The original SQL query string to which pagination and sorting will be added. + * @param PicoPageable|null $pageable The pagination parameters, or `null` if pagination is not required. + * @param PicoSortable|null $sortable The sorting parameters, or `null` if sorting is not required. + * + * @return string The modified SQL query string with added pagination and sorting clauses. + */ + public function addPaginationAndSorting($queryString, $pageable, $sortable) + { + if(!isset($pageable) && !isset($sortable)) + { + return $queryString; + } + + $queryString = rtrim($queryString, " \r\n\t; "); + + if(isset($sortable)) + { + foreach($sortable->getSortable() as $sort) + { + $columnName = $sort->getSortBy(); + $sortType = $sort->getSortType(); + $sorts[] = $columnName . " " . $sortType; + } + if(!empty($sorts)) + { + $queryString .= "\r\nORDER BY ".implode(", ", $sorts); + } + } + if(isset($pageable)) + { + $limitOffset = $pageable->getOffsetLimit(); + $limit = $limitOffset->getLimit(); + $offset = $limitOffset->getOffset(); + if($this->isPgSql() || $this->isSqlite()) + { + // PostgeSQL and SQLite + $queryString .= "\r\nLIMIT $limit OFFSET $offset"; + } + else if($this->isMySql()) + { + // MariaDB and MySQL + $queryString .= "\r\nLIMIT $offset, $limit"; + } + } + + return $queryString; + } + /** * Get the current SQL query as a string. * diff --git a/src/Database/PicoDatabaseType.php b/src/Database/PicoDatabaseType.php index 06bbb79a..48348ad1 100644 --- a/src/Database/PicoDatabaseType.php +++ b/src/Database/PicoDatabaseType.php @@ -42,6 +42,13 @@ class PicoDatabaseType */ const DATABASE_TYPE_POSTGRESQL = "postgresql"; + /** + * Constant for PostgreSQL database type. + * + * @var string + */ + const DATABASE_TYPE_PGSQL = "pgsql"; + /** * Constant for SQLite database type. * diff --git a/src/Generator/PicoDatabaseDump.php b/src/Generator/PicoDatabaseDump.php index 15374cd6..112c4733 100644 --- a/src/Generator/PicoDatabaseDump.php +++ b/src/Generator/PicoDatabaseDump.php @@ -66,7 +66,7 @@ public function dumpStructure($entity, $databaseType, $createIfNotExists = false $tool = new PicoDatabaseUtilMySql(); return $tool->dumpStructure($tableInfo, $picoTableName, $createIfNotExists, $dropIfExists, $engine, $charset); } - else if($databaseType == PicoDatabaseType::DATABASE_TYPE_POSTGRESQL) + else if($databaseType == PicoDatabaseType::DATABASE_TYPE_PGSQL) { $tool = new PicoDatabaseUtilPostgreSql(); return $tool->dumpStructure($tableInfo, $picoTableName, $createIfNotExists, $dropIfExists, $engine, $charset); @@ -92,7 +92,7 @@ public function dumpStructureTable($tableInfo, $databaseType, $createIfNotExists $tool = new PicoDatabaseUtilMySql(); return $tool->dumpStructure($tableInfo, $picoTableName, $createIfNotExists, $dropIfExists, $engine, $charset); } - else if($databaseType == PicoDatabaseType::DATABASE_TYPE_POSTGRESQL) + else if($databaseType == PicoDatabaseType::DATABASE_TYPE_PGSQL) { $tool = new PicoDatabaseUtilPostgreSql(); return $tool->dumpStructure($tableInfo, $picoTableName, $createIfNotExists, $dropIfExists, $engine, $charset); @@ -370,7 +370,7 @@ private function addAutoIncrement($queryAlter, $tableInfo, $tableName, $createdC $query = $this->updateQueryAlterTableNullable($query, $entityColumn); $query = $this->updateQueryAlterTableDefaultValue($query, $entityColumn); - if ($databaseType == PicoDatabaseType::DATABASE_TYPE_POSTGRESQL) { + if ($databaseType == PicoDatabaseType::DATABASE_TYPE_PGSQL) { $columnName = $entityColumn['name']; $sequenceName = $tableName . "_" . $columnName; $queries[] = ""; diff --git a/src/Generator/PicoDtoGenerator.php b/src/Generator/PicoDtoGenerator.php index 5831abac..44cb01f5 100644 --- a/src/Generator/PicoDtoGenerator.php +++ b/src/Generator/PicoDtoGenerator.php @@ -85,16 +85,8 @@ class PicoDtoGenerator * @param string|null $entityName Name of the entity (optional) * @param bool $prettify Flag to prettify output (default: false) */ - public function __construct( - $database, - $baseDir, - $tableName, - $baseNamespaceDto, - $dtoName, - $baseNamespaceEntity, - $entityName = null, - $prettify = false - ) { + public function __construct($database, $baseDir, $tableName, $baseNamespaceDto, $dtoName, $baseNamespaceEntity, $entityName = null, $prettify = false) //NOSONAR + { $this->database = $database; $this->baseDir = $baseDir; $this->tableName = $tableName; diff --git a/src/Geometry/LatBound.php b/src/Geometry/LatBound.php index 803895ff..be45b91f 100644 --- a/src/Geometry/LatBound.php +++ b/src/Geometry/LatBound.php @@ -80,28 +80,28 @@ public function isEmpty() /** * Determine if this LatBounds intersects with another LatBounds. * - * @param LatBounds $LatBounds The other LatBounds to check for intersection. + * @param LatBounds $latBounds The other LatBounds to check for intersection. * @return bool True if there is an intersection, false otherwise. */ - public function intersects($LatBounds) + public function intersects($latBounds) { - return $this->_swLat <= $LatBounds->getSw() - ? $LatBounds->getSw() <= $this->_neLat && $LatBounds->getSw() <= $LatBounds->getNe() - : $this->_swLat <= $LatBounds->getNe() && $this->_swLat <= $this->_neLat; + return $this->_swLat <= $latBounds->getSw() + ? $latBounds->getSw() <= $this->_neLat && $latBounds->getSw() <= $latBounds->getNe() + : $this->_swLat <= $latBounds->getNe() && $this->_swLat <= $this->_neLat; } /** * Check if this LatBounds is equal to another LatBounds within a certain margin of error. * - * @param LatBounds $LatBounds The other LatBounds to compare. + * @param LatBounds $latBounds The other LatBounds to compare. * @return bool True if they are equal, false otherwise. */ - public function equals($LatBounds) + public function equals($latBounds) { return $this->isEmpty() - ? $LatBounds->isEmpty() - : abs($LatBounds->getSw() - $this->_swLat) - + abs($this->_neLat - $LatBounds->getNe()) + ? $latBounds->isEmpty() + : abs($latBounds->getSw() - $this->_swLat) + + abs($this->_neLat - $latBounds->getNe()) <= SphericalGeometry::EQUALS_MARGIN_ERROR; } diff --git a/src/Geometry/LatLng.php b/src/Geometry/LatLng.php index 7cbabf97..56201ebf 100644 --- a/src/Geometry/LatLng.php +++ b/src/Geometry/LatLng.php @@ -75,17 +75,17 @@ public function getLng() /** * Check if this LatLng is equal to another LatLng object within a certain margin of error. * - * @param LatLng $LatLng The LatLng object to compare. + * @param LatLng $latLng The LatLng object to compare. * @return bool True if they are equal, false otherwise. */ - public function equals($LatLng) + public function equals($latLng) { - if (!is_object($LatLng) || !($LatLng instanceof self)) { + if (!is_object($latLng) || !($latLng instanceof self)) { return false; } - return abs($this->_lat - $LatLng->getLat()) <= SphericalGeometry::EQUALS_MARGIN_ERROR - && abs($this->_lng - $LatLng->getLng()) <= SphericalGeometry::EQUALS_MARGIN_ERROR; + return abs($this->_lat - $latLng->getLat()) <= SphericalGeometry::EQUALS_MARGIN_ERROR + && abs($this->_lng - $latLng->getLng()) <= SphericalGeometry::EQUALS_MARGIN_ERROR; } /** diff --git a/src/Geometry/LatLngBounds.php b/src/Geometry/LatLngBounds.php index 127c3dde..c64dbfd3 100644 --- a/src/Geometry/LatLngBounds.php +++ b/src/Geometry/LatLngBounds.php @@ -26,32 +26,32 @@ class LatLngBounds /** * LatLngBounds constructor. * - * @param LatLng|null $LatLngSw The southwestern LatLng object. - * @param LatLng|null $LatLngNe The northeastern LatLng object. + * @param LatLng|null $latLngSw The southwestern LatLng object. + * @param LatLng|null $tatLngNe The northeastern LatLng object. * * @throws E_USER_ERROR If the provided LatLng objects are invalid. */ - public function __construct($LatLngSw = null, $LatLngNe = null) + public function __construct($latLngSw = null, $tatLngNe = null) { - if ((!is_null($LatLngSw) && !($LatLngSw instanceof LatLng)) - || (!is_null($LatLngNe) && !($LatLngNe instanceof LatLng))) + if ((!is_null($latLngSw) && !($latLngSw instanceof LatLng)) + || (!is_null($tatLngNe) && !($tatLngNe instanceof LatLng))) { trigger_error('LatLngBounds class -> Invalid LatLng object.', E_USER_ERROR); } - if ($LatLngSw && !$LatLngNe) + if ($latLngSw && !$tatLngNe) { - $LatLngNe = $LatLngSw; + $tatLngNe = $latLngSw; } - if ($LatLngSw) + if ($latLngSw) { - $sw = SphericalGeometry::clampLatitude($LatLngSw->getLat()); - $ne = SphericalGeometry::clampLatitude($LatLngNe->getLat()); + $sw = SphericalGeometry::clampLatitude($latLngSw->getLat()); + $ne = SphericalGeometry::clampLatitude($tatLngNe->getLat()); $this->_LatBounds = new LatBounds($sw, $ne); - $sw = $LatLngSw->getLng(); - $ne = $LatLngNe->getLng(); + $sw = $latLngSw->getLng(); + $ne = $tatLngNe->getLng(); if ($ne - $sw >= 360) { @@ -59,8 +59,8 @@ public function __construct($LatLngSw = null, $LatLngNe = null) } else { - $sw = SphericalGeometry::wrapLongitude($LatLngSw->getLng()); - $ne = SphericalGeometry::wrapLongitude($LatLngNe->getLng()); + $sw = SphericalGeometry::wrapLongitude($latLngSw->getLng()); + $ne = SphericalGeometry::wrapLongitude($tatLngNe->getLng()); $this->_LngBounds = new LngBounds($sw, $ne); } } @@ -183,64 +183,64 @@ public function toUrlValue($precision = 6) /** * Check if this LatLngBounds is equal to another LatLngBounds object. * - * @param LatLngBounds $LatLngBounds The LatLngBounds object to compare. + * @param LatLngBounds $latLngBounds The LatLngBounds object to compare. * @return bool True if they are equal, false otherwise. */ - public function equals($LatLngBounds) + public function equals($latLngBounds) { - return !$LatLngBounds + return !$latLngBounds ? false - : $this->_LatBounds->equals($LatLngBounds->getLatBounds()) - && $this->_LngBounds->equals($LatLngBounds->getLngBounds()); + : $this->_LatBounds->equals($latLngBounds->getLatBounds()) + && $this->_LngBounds->equals($latLngBounds->getLngBounds()); } /** * Check if this LatLngBounds intersects with another LatLngBounds. * - * @param LatLngBounds $LatLngBounds The LatLngBounds to check for intersection. + * @param LatLngBounds $latLngBounds The LatLngBounds to check for intersection. * @return bool True if they intersect, false otherwise. */ - public function intersects($LatLngBounds) + public function intersects($latLngBounds) { - return $this->_LatBounds->intersects($LatLngBounds->getLatBounds()) - && $this->_LngBounds->intersects($LatLngBounds->getLngBounds()); + return $this->_LatBounds->intersects($latLngBounds->getLatBounds()) + && $this->_LngBounds->intersects($latLngBounds->getLngBounds()); } /** * Extend this bounding box to include another LatLngBounds. * - * @param LatLngBounds $LatLngBounds The LatLngBounds to extend with. + * @param LatLngBounds $latLngBounds The LatLngBounds to extend with. * @return $this The current instance for method chaining. */ - public function union($LatLngBounds) + public function union($latLngBounds) { - $this->extend($LatLngBounds->getSouthWest()); - $this->extend($LatLngBounds->getNorthEast()); + $this->extend($latLngBounds->getSouthWest()); + $this->extend($latLngBounds->getNorthEast()); return $this; } /** * Check if this LatLngBounds contains a specific LatLng point. * - * @param LatLng $LatLng The LatLng point to check for containment. + * @param LatLng $latLng The LatLng point to check for containment. * @return bool True if the point is contained, false otherwise. */ - public function contains($LatLng) + public function contains($latLng) { - return $this->_LatBounds->contains($LatLng->getLat()) - && $this->_LngBounds->contains($LatLng->getLng()); + return $this->_LatBounds->contains($latLng->getLat()) + && $this->_LngBounds->contains($latLng->getLng()); } /** * Extend the bounding box to include a new LatLng point. * - * @param LatLng $LatLng The LatLng point to extend with. + * @param LatLng $latLng The LatLng point to extend with. * @return $this The current instance for method chaining. */ - public function extend($LatLng) + public function extend($latLng) { - $this->_LatBounds->extend($LatLng->getLat()); - $this->_LngBounds->extend($LatLng->getLng()); + $this->_LatBounds->extend($latLng->getLat()); + $this->_LngBounds->extend($latLng->getLng()); return $this; } } diff --git a/src/Geometry/LngBound.php b/src/Geometry/LngBound.php index 2e420401..0d32a6ac 100644 --- a/src/Geometry/LngBound.php +++ b/src/Geometry/LngBound.php @@ -88,43 +88,43 @@ public function isEmpty() /** * Check if this LngBounds intersects with another LngBounds. * - * @param LngBounds $LngBounds The LngBounds to check for intersection. + * @param LngBounds $lngBounds The LngBounds to check for intersection. * @return bool True if they intersect, false otherwise. */ - public function intersects($LngBounds) // NOSONAR + public function intersects($lngBounds) // NOSONAR { - if ($this->isEmpty() || $LngBounds->isEmpty()) + if ($this->isEmpty() || $lngBounds->isEmpty()) { return false; } else if ($this->_swLng > $this->_neLng) { - return $LngBounds->getSw() > $LngBounds->getNe() - || $LngBounds->getSw() <= $this->_neLng - || $LngBounds->getNe() >= $this->_swLng; + return $lngBounds->getSw() > $lngBounds->getNe() + || $lngBounds->getSw() <= $this->_neLng + || $lngBounds->getNe() >= $this->_swLng; } - else if ($LngBounds->getSw() > $LngBounds->getNe()) + else if ($lngBounds->getSw() > $lngBounds->getNe()) { - return $LngBounds->getSw() <= $this->_neLng || $LngBounds->getNe() >= $this->_swLng; + return $lngBounds->getSw() <= $this->_neLng || $lngBounds->getNe() >= $this->_swLng; } else { - return $LngBounds->getSw() <= $this->_neLng && $LngBounds->getNe() >= $this->_swLng; + return $lngBounds->getSw() <= $this->_neLng && $lngBounds->getNe() >= $this->_swLng; } } /** * Check if this LngBounds is equal to another LngBounds. * - * @param LngBounds $LngBounds The LngBounds object to compare. + * @param LngBounds $lngBounds The LngBounds object to compare. * @return bool True if they are equal, false otherwise. */ - public function equals($LngBounds) + public function equals($lngBounds) { return $this->isEmpty() - ? $LngBounds->isEmpty() - : fmod(abs($LngBounds->getSw() - $this->_swLng), 360) - + fmod(abs($LngBounds->getNe() - $this->_neLng), 360) + ? $lngBounds->isEmpty() + : fmod(abs($lngBounds->getSw() - $this->_swLng), 360) + + fmod(abs($lngBounds->getNe() - $this->_neLng), 360) <= SphericalGeometry::EQUALS_MARGIN_ERROR; } diff --git a/src/MagicDto.php b/src/MagicDto.php index b8109860..98e9ebdd 100644 --- a/src/MagicDto.php +++ b/src/MagicDto.php @@ -135,17 +135,6 @@ public function loadData($data) return $this; } - private function isMyChild($object) { - $reflection = new ReflectionClass($object); - $parentClass = $reflection->getParentClass(); - if(!$parentClass) - { - return false; - } - return get_class($parentClass) == get_class(new self()); - } - - /** * Loads XML data into the object. * diff --git a/src/MagicObject.php b/src/MagicObject.php index 3c014664..d8122a3b 100644 --- a/src/MagicObject.php +++ b/src/MagicObject.php @@ -2,7 +2,6 @@ namespace MagicObject; -use DateTime; use Exception; use PDOException; use PDOStatement; @@ -17,6 +16,7 @@ use MagicObject\Database\PicoSortable; use MagicObject\Database\PicoSpecification; use MagicObject\Database\PicoTableInfo; +use MagicObject\Exceptions\FileNotFoundException; use MagicObject\Exceptions\FindOptionException; use MagicObject\Exceptions\InvalidAnnotationException; use MagicObject\Exceptions\InvalidQueryInputException; @@ -25,6 +25,7 @@ use MagicObject\Exceptions\NoRecordFoundException; use MagicObject\Util\ClassUtil\PicoAnnotationParser; use MagicObject\Util\ClassUtil\PicoObjectParser; +use MagicObject\Util\Database\NativeQueryUtil; use MagicObject\Util\Database\PicoDatabaseUtil; use MagicObject\Util\PicoArrayUtil; use MagicObject\Util\PicoEnvironmentVariable; @@ -144,10 +145,24 @@ public function nullPropertyList() /** * Constructor. * - * Initializes the object with provided data and database connection. + * Initializes the object with the provided data and optionally connects to a database. + * The constructor can accept different types of data to populate the object and can + * also accept a PDO connection or a PicoDatabase instance to set up the database connection. * - * @param self|array|stdClass|object|null $data Initial data to populate the object. - * @param PicoDatabase|null $database Database connection instance. + * @param self|array|stdClass|object|null $data Initial data to populate the object. This can be: + * - `self`: An instance of the same class to clone data. + * - `array`: An associative array of data, which will be camel-cased. + * - `stdClass`: A standard object to populate the properties. + * - `object`: A generic object to populate the properties. + * - `null`: No data, leaving the object empty. + * + * @param PicoDatabase|PDO|null $database A database connection instance, either: + * - `PicoDatabase`: An already instantiated PicoDatabase object. + * - `PDO`: A PDO connection object, which will be converted into a PicoDatabase instance using `PicoDatabase::fromPdo()`. + * - `null`: No database connection. + * + * @throws InvalidAnnotationException If the annotations are invalid or cannot be parsed. + * @throws InvalidQueryInputException If an error occurs while parsing the key-value pair annotations. */ public function __construct($data = null, $database = null) { @@ -173,9 +188,16 @@ public function __construct($data = null, $database = null) } $this->loadData($data); } - if($database != null && $database instanceof PicoDatabase) + if($database != null) { - $this->_database = $database; + if($database instanceof PicoDatabase) + { + $this->_database = $database; + } + else if($database instanceof PDO) + { + $this->_database = PicoDatabase::fromPdo($database); + } } } @@ -218,7 +240,7 @@ public function loadIniString($rawData, $systemEnv = false) { // Parse without sections $data = PicoIniUtil::parseIniString($rawData); - if(isset($data) && !empty($data)) + if($this->_isNotNullAndNotEmpty($data)) { $data = PicoEnvironmentVariable::replaceValueAll($data, $data, true); if($systemEnv) @@ -242,7 +264,7 @@ public function loadIniFile($path, $systemEnv = false) { // Parse without sections $data = PicoIniUtil::parseIniFile($path); - if(isset($data) && !empty($data)) + if($this->_isNotNullAndNotEmpty($data)) { $data = PicoEnvironmentVariable::replaceValueAll($data, $data, true); if($systemEnv) @@ -267,7 +289,7 @@ public function loadIniFile($path, $systemEnv = false) public function loadYamlString($rawData, $systemEnv = false, $asObject = false, $recursive = false) { $data = Yaml::parse($rawData); - if(isset($data) && !empty($data)) + if($this->_isNotNullAndNotEmpty($data)) { $data = PicoEnvironmentVariable::replaceValueAll($data, $data, true); if($systemEnv) @@ -315,7 +337,7 @@ public function loadYamlString($rawData, $systemEnv = false, $asObject = false, public function loadYamlFile($path, $systemEnv = false, $asObject = false, $recursive = false) { $data = Yaml::parseFile($path); - if(isset($data) && !empty($data)) + if($this->_isNotNullAndNotEmpty($data)) { $data = PicoEnvironmentVariable::replaceValueAll($data, $data, true); if($systemEnv) @@ -363,7 +385,7 @@ public function loadYamlFile($path, $systemEnv = false, $asObject = false, $recu public function loadJsonString($rawData, $systemEnv = false, $asObject = false, $recursive = false) { $data = json_decode($rawData); - if(isset($data) && !empty($data)) + if($this->_isNotNullAndNotEmpty($data)) { $data = PicoEnvironmentVariable::replaceValueAll($data, $data, true); if($systemEnv) @@ -400,50 +422,73 @@ public function loadJsonString($rawData, $systemEnv = false, $asObject = false, } /** - * Load data from a JSON file. + * Loads data from a JSON file and processes it based on the provided options. * - * @param string $path File path to the JSON file - * @param bool $systemEnv Replace all environment variable values - * @param bool $asObject Result as an object instead of an array - * @param bool $recursive Convert all objects to MagicObject + * This method reads the contents of a JSON file, decodes it, and applies transformations + * such as replacing environment variables, camelizing the keys, and recursively converting objects + * into MagicObject instances if necessary. + * + * @param string $path The file path to the JSON file. + * @param bool $systemEnv Whether to replace system environment variables in the data (default: `false`). + * @param bool $asObject Whether to return the result as an object instead of an associative array (default: `false`). + * @param bool $recursive Whether to recursively convert all objects into MagicObject instances (default: `false`). + * * @return self Returns the current instance for method chaining. + * + * @throws FileNotFoundException If the specified JSON file does not exist. */ public function loadJsonFile($path, $systemEnv = false, $asObject = false, $recursive = false) { - $data = json_decode(file_get_contents($path)); - if(isset($data) && !empty($data)) - { + // Check if the file exists + if (!file_exists($path)) { + throw new FileNotFoundException("Specified file not found [{$path}]"); + } + + // Decode the JSON file contents as an associative array + $data = json_decode(file_get_contents($path), true); // true to decode as associative array + + // If data is valid and not empty, process it + if (!empty($data)) { + // Replace Pico environment variables in the data $data = PicoEnvironmentVariable::replaceValueAll($data, $data, true); - if($systemEnv) - { + + // If systemEnv is true, replace system environment variables + if ($systemEnv) { $data = PicoEnvironmentVariable::replaceSysEnvAll($data, true); } + + // Camelize the data keys (e.g., 'user_name' to 'userName') $data = PicoArrayUtil::camelize($data); - if($asObject) - { - // convert to object - $obj = json_decode(json_encode((object) $data), false); - if($recursive) - { - $this->loadData(PicoObjectParser::parseRecursiveObject($obj)); - } - else - { - $this->loadData($obj); - } - } - else - { - if($recursive) - { - $this->loadData(PicoObjectParser::parseRecursiveObject($data)); - } - else - { - $this->loadData($data); - } - } + + // Load the processed data (object or array, recursively if needed) + return $this->loadJsonData($data, $asObject, $recursive); } + + return $this; + } + + /** + * Loads processed JSON data and optionally converts it to objects or parses recursively. + * + * @param mixed $data The processed data to load (array or object). + * @param bool $asObject Whether to return the result as an object. + * @param bool $recursive Whether to recursively convert all objects into MagicObject instances. + * + * @return self Returns the current instance for method chaining. + */ + private function loadJsonData($data, $asObject, $recursive) + { + if ($asObject) { + // Convert data to object + $data = json_decode(json_encode($data), false); // Convert array to object + } + + // Load data, applying recursion if needed + $dataToLoad = $recursive ? PicoObjectParser::parseRecursiveObject($data) : $data; + + // Call the loadData method to process the data + $this->loadData($dataToLoad); + return $this; } @@ -462,6 +507,16 @@ protected function readOnly($readonly) return $this; } + /** + * Check if database is connected or not + * + * @return bool + */ + private function _databaseConnected() + { + return $this->_database != null && $this->_database->isConnected(); + } + /** * Set the database connection. * @@ -593,7 +648,7 @@ public function save($includeNull = false) */ public function saveQuery($includeNull = false) { - if($this->_database != null && ($this->_database->getDatabaseType() != null && $this->_database->getDatabaseType() != "")) + if($this->_databaseConnected()) { $persist = new PicoDatabasePersistence($this->_database, $this); return $persist->saveQuery($includeNull); @@ -662,7 +717,7 @@ public function selectAll() */ public function selectQuery() { - if($this->_database != null && ($this->_database->getDatabaseType() != null && $this->_database->getDatabaseType() != "")) + if($this->_databaseConnected()) { $persist = new PicoDatabasePersistence($this->_database, $this); return $persist->selectQuery(); @@ -676,10 +731,10 @@ public function selectQuery() /** * Executes a database query based on the parameters and annotations from the caller function. * - * This method uses reflection to retrieve the query string from the caller's docblock, - * bind the parameters, and execute the query against the database. + * This method uses reflection to extract the query string and return type from the caller's + * docblock, binds the provided parameters, and executes the query against the database. * - * It analyzes the parameters and return type of the caller function, enabling dynamic query + * It analyzes the parameters and return type of the caller function to enable dynamic query * execution tailored to the specified return type. Supported return types include: * - `void`: Returns null. * - `int` or `integer`: Returns the number of affected rows. @@ -688,235 +743,91 @@ public function selectQuery() * - `array`: Returns all results as an associative array. * - `string`: Returns the JSON-encoded results. * - `PDOStatement`: Returns the prepared statement for further operations if needed. - * - `MagicObject` and its derived classes: If the return type is a class name or an array of class names, - * instances of that class will be created for each row fetched. + * - `MagicObject` and its derived classes: If the return type is a class name or an array of + * class names, instances of that class will be created for each row fetched. + * - `MagicObject[]` and its derived classes: Instances of the corresponding class will be + * created for each row fetched. * - * @return mixed Returns the result based on the return type of the caller function: + * @return mixed The result based on the return type of the caller function: * - null if the return type is void. * - integer for the number of affected rows if the return type is int. * - object for a single result if the return type is object. * - an array of associative arrays for multiple results if the return type is array. * - a JSON string if the return type is string. * - instances of a specified class if the return type matches a class name. - * + * * @throws PDOException If there is an error executing the database query. - * @throws InvalidQueryInputException If there is no query to be executed. - * @throws InvalidReturnTypeException If the return type specified is invalid. + * @throws InvalidQueryInputException If there is no query to be executed or if the input is invalid. + * @throws InvalidReturnTypeException If the return type specified in the docblock is invalid or unrecognized. */ - protected function executeNativeQuery() //NOSONAR + protected function executeNativeQuery() { // Retrieve caller trace information $trace = debug_backtrace(); + $traceCaller = $trace[1]; - // Get parameters from the caller function - $callerParamValues = isset($trace[1]['args']) ? $trace[1]['args'] : []; + // Extract the caller's parameters + $callerParamValues = isset($traceCaller['args']) ? $traceCaller['args'] : []; - // Get the name of the caller function and class - $callerFunctionName = $trace[1]['function']; - $callerClassName = $trace[1]['class']; + // Get the caller's function and class names + $callerFunctionName = $traceCaller['function']; + $callerClassName = $traceCaller['class']; - // Use reflection to get annotations from the caller function + // Use reflection to retrieve docblock annotations from the caller function $reflection = new ReflectionMethod($callerClassName, $callerFunctionName); - $docComment = $reflection->getDocComment(); - - // Get the query from the @query annotation - preg_match('/@query\s*\("([^"]+)"\)/', $docComment, $matches); - $queryString = $matches ? $matches[1] : ''; - $queryString = trim($queryString, " \r\n\t "); - if(empty($queryString)) - { - // Try reading the query in another way - preg_match('/@query\s*\(\s*"(.*?)"\s*\)/s', $docComment, $matches); - $queryString = $matches ? $matches[1] : ''; - if(empty($queryString)) - { - throw new InvalidQueryInputException("No query found.\r\n".$docComment); - } - } - // Get parameter information from the caller function $callerParams = $reflection->getParameters(); - // Get return type from the caller function - preg_match('/@return\s+([^\s]+)/', $docComment, $matches); - $returnType = $matches ? $matches[1] : 'void'; - - // Trim return type - $returnType = trim($returnType); - - // Change self to callerClassName - if($returnType == "self[]") - { - $returnType = $callerClassName."[]"; - } - else if($returnType == "self") - { - $returnType = $callerClassName; - } + // Get the docblock comment for the caller function + $docComment = $reflection->getDocComment(); + + $nativeQueryUtil = new NativeQueryUtil(); + // Extract the query string and return type from the docblock + $queryString = $nativeQueryUtil->extractQueryString($docComment); + $returnType = $nativeQueryUtil->extractReturnType($docComment, $callerClassName); + $params = []; try { - // Get database connection - $pdo = $this->_database->getDatabaseConnection(); - - // Replace array - foreach ($callerParamValues as $index => $paramValue) { - if (isset($callerParams[$index])) { - // Format parameter name according to the query - $paramName = $callerParams[$index]->getName(); - if(is_array($paramValue)) - { - $queryString = str_replace(":".$paramName, PicoDatabaseUtil::toList($paramValue, true, true), $queryString); - } - } - } + // Apply query parameters (pagination, sorting, etc.) + $queryString = $nativeQueryUtil->applyQueryParameters($this->_database->getDatabaseType(), $queryString, $callerParams, $callerParamValues); + // Prepare the query using the database connection + $pdo = $this->_database->getDatabaseConnection(); $stmt = $pdo->prepare($queryString); - // Automatically bind each parameter + // Bind the parameters to the prepared statement foreach ($callerParamValues as $index => $paramValue) { - if (isset($callerParams[$index])) { - // Format parameter name according to the query + if (isset($callerParams[$index]) && !($paramValue instanceof PicoPageable) && !($paramValue instanceof PicoSortable)) { + // Bind the parameter name and type to the statement $paramName = $callerParams[$index]->getName(); - if(!is_array($paramValue)) - { - $maped = $this->mapToPdoParamType($paramValue); - $paramType = $maped->type; - $paramValue = $maped->value; + if (!is_array($paramValue)) { + $mapped = $nativeQueryUtil->mapToPdoParamType($paramValue); + $paramType = $mapped->type; + $paramValue = $mapped->value; + + // Debugging: store parameter values for query inspection $params[$paramName] = $paramValue; - $stmt->bindValue(":".$paramName, $paramValue, $paramType); + $stmt->bindValue(":" . $paramName, $paramValue, $paramType); } } } - - // Send query to logger - $debugFunction = $this->_database->getCallbackDebugQuery(); - if(isset($debugFunction) && is_callable($debugFunction)) - { - call_user_func($debugFunction, PicoDatabaseUtil::getFinalQuery($stmt, $params)); - } + + // Log the query for debugging + $nativeQueryUtil->debugQuery($this->_database, $stmt, $params); // Execute the query $stmt->execute(); - if ($returnType == "void") { - // Return null if the return type is void - return null; - } - if ($returnType == "PDOStatement") { - // Return the PDOStatement object - return $stmt; - } else if ($returnType == "int" || $returnType == "integer") { - // Return the affected row count - return $stmt->rowCount(); - } else if ($returnType == "object" || $returnType == "stdClass") { - // Return one row as an object - return $stmt->fetch(PDO::FETCH_OBJ); - } else if ($returnType == "array") { - // Return all rows as an associative array - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } else if ($returnType == "string") { - // Return the result as a JSON string - return json_encode($stmt->fetchAll(PDO::FETCH_OBJ)); - } else { - try { - // Check for array-type hinting in the return type - if (stripos($returnType, "[") !== false) { - $className = trim(explode("[", $returnType)[0]); - if ($className == "stdClass") { - // Return all rows as stdClass objects - return $stmt->fetchAll(PDO::FETCH_OBJ); - } - else if($className == 'MagicObject') { - $result = $stmt->fetchAll(PDO::FETCH_OBJ); - $ret = []; - foreach ($result as $row) { - $ret[] = new MagicObject($row); - } - return $ret; - } - else if (class_exists($className)) { - // Map result rows to the specified class - $obj = new $className(); - if($obj instanceof MagicObject) { - $result = $stmt->fetchAll(PDO::FETCH_OBJ); - foreach ($result as $row) { - $ret[] = new $className($row); - } - return $ret; - } - } - throw new InvalidReturnTypeException("Invalid return type for $className"); - } else { - // Return a single object of the specified class - $className = trim($returnType); - if($className == 'MagicObject') { - $row = $stmt->fetch(PDO::FETCH_OBJ); - return new MagicObject($row); - } - else if (class_exists($className)) { - $obj = new $className(); - if($obj instanceof MagicObject) { - $row = $stmt->fetch(PDO::FETCH_OBJ); - return $obj->loadData($row); - } - } - throw new InvalidReturnTypeException("Invalid return type for $className"); - } - } catch (Exception $e) { - // Log the exception if the class is not found - throw new InvalidReturnTypeException("Invalid return type for $className"); - } - } + // Handle and return the result based on the specified return type + return $nativeQueryUtil->handleReturnObject($stmt, $returnType); } - catch (PDOException $e) - { - // Handle database errors with logging + catch (PDOException $e) { + // Log and rethrow the exception if a database error occurs throw new PDOException($e->getMessage(), $e->getCode(), $e); } - return null; - } - - /** - * Maps PHP types to PDO parameter types. - * - * This function determines the appropriate PDO parameter type based on the given value. - * It handles various PHP data types and converts them to the corresponding PDO parameter types - * required for executing prepared statements in PDO. - * - * @param mixed $value The value to determine the type for. This can be of any type, including - * null, boolean, integer, string, DateTime, or other types. - * @return stdClass An object containing: - * - type: The PDO parameter type (PDO::PARAM_STR, PDO::PARAM_NULL, - * PDO::PARAM_BOOL, PDO::PARAM_INT). - * - value: The corresponding value formatted as needed for the PDO parameter. - */ - private function mapToPdoParamType($value) - { - $type = PDO::PARAM_STR; // Default type is string - $finalValue = $value; // Initialize final value to the original value - - if ($value instanceof DateTime) { - $type = PDO::PARAM_STR; // DateTime should be treated as a string - $finalValue = $value->format("Y-m-d H:i:s"); - } else if (is_null($value)) { - $type = PDO::PARAM_NULL; // NULL type - $finalValue = null; // Set final value to null - } else if (is_bool($value)) { - $type = PDO::PARAM_BOOL; // Boolean type - $finalValue = $value; // Keep the boolean value - } else if (is_int($value)) { - $type = PDO::PARAM_INT; // Integer type - $finalValue = $value; // Keep the integer value - } - - // Create and return an object with the type and value - $result = new stdClass(); - $result->type = $type; - $result->value = $finalValue; - return $result; } /** @@ -948,7 +859,7 @@ public function insert($includeNull = false) */ public function insertQuery($includeNull = false) { - if($this->_database != null && ($this->_database->getDatabaseType() != null && $this->_database->getDatabaseType() != "")) + if($this->_databaseConnected()) { $persist = new PicoDatabasePersistence($this->_database, $this); return $persist->insertQuery($includeNull); @@ -988,7 +899,7 @@ public function update($includeNull = false) */ public function updateQuery($includeNull = false) { - if($this->_database != null && ($this->_database->getDatabaseType() != null && $this->_database->getDatabaseType() != "")) + if($this->_databaseConnected()) { $persist = new PicoDatabasePersistence($this->_database, $this); return $persist->updateQuery($includeNull); @@ -1026,7 +937,7 @@ public function delete() */ public function deleteQuery() { - if($this->_database != null && ($this->_database->getDatabaseType() != null && $this->_database->getDatabaseType() != "")) + if($this->_databaseConnected()) { $persist = new PicoDatabasePersistence($this->_database, $this); return $persist->deleteQuery(); @@ -1036,6 +947,100 @@ public function deleteQuery() throw new NoDatabaseConnectionException(self::MESSAGE_NO_DATABASE_CONNECTION); } } + + /** + * Starts a database transaction. + * + * This method begins a new database transaction. It delegates the actual transaction + * initiation to the `transactionalCommand` method, passing the "start" command. + * + * @return self The current instance of the class for method chaining. + * + * @throws NoDatabaseConnectionException If there is no active database connection. + * @throws PDOException If there is an error while starting the transaction. + */ + public function startTransaction() + { + $this->transactionalCommand("start"); + return $this; + } + + /** + * Commits the current database transaction. + * + * This method commits the current transaction. If successful, it makes all database + * changes made during the transaction permanent. It delegates to the `transactionalCommand` method + * with the "commit" command. + * + * @return self The current instance of the class for method chaining. + * + * @throws NoDatabaseConnectionException If there is no active database connection. + * @throws PDOException If there is an error during the commit process. + */ + public function commit() + { + $this->transactionalCommand("commit"); + return $this; + } + + /** + * Rolls back the current database transaction. + * + * This method rolls back the current transaction, undoing all database changes made + * during the transaction. It calls the `transactionalCommand` method with the "rollback" command. + * + * @return self The current instance of the class for method chaining. + * + * @throws NoDatabaseConnectionException If there is no active database connection. + * @throws PDOException If there is an error during the rollback process. + */ + public function rollback() + { + $this->transactionalCommand("rollback"); + return $this; + } + + /** + * Executes a transactional SQL command (start, commit, or rollback). + * + * This method executes a SQL command to manage the state of a database transaction. + * It checks the type of command (`start_transaction`, `commit`, or `rollback`) and + * delegates the corresponding SQL generation to the `PicoDatabaseQueryBuilder` class. + * The SQL statement is then executed on the active database connection. + * + * @param string $command The transactional command to execute. Possible values are: + * - "start" to begin a new transaction. + * - "commit" to commit the current transaction. + * - "rollback" to rollback the current transaction. + * + * @return void + * + * @throws NoDatabaseConnectionException If there is no active database connection. + * @throws PDOException If there is an error while executing the transactional command. + */ + private function transactionalCommand($command) + { + if ($this->_databaseConnected()) { + try { + $queryBuilder = new PicoDatabaseQueryBuilder($this->_database); + $sql = null; + if ($command == "start") { + $sql = $queryBuilder->startTransaction(); + } elseif ($command == "commit") { + $sql = $queryBuilder->commit(); + } elseif ($command == "rollback") { + $sql = $queryBuilder->rollback(); + } + if (isset($sql)) { + $this->_database->execute($sql); + } + } catch (Exception $e) { + throw new PDOException($e); + } + } else { + throw new NoDatabaseConnectionException(self::MESSAGE_NO_DATABASE_CONNECTION); + } + } /** * Get MagicObject with WHERE specification. @@ -1045,7 +1050,7 @@ public function deleteQuery() */ public function where($specification) { - if($this->_database != null && ($this->_database->getDatabaseType() != null && $this->_database->getDatabaseType() != "")) + if($this->_databaseConnected()) { $persist = new PicoDatabasePersistenceExtended($this->_database, $this); return $persist->whereWithSpecification($specification); @@ -1410,7 +1415,7 @@ public function valueObject($snakeCase = null) $obj->set($key, $value); } } - $upperCamel = $this->isUpperCamel(); + $upperCamel = $this->_upperCamel(); if($upperCamel) { return json_decode(json_encode($this->valueArrayUpperCamel())); @@ -1485,7 +1490,7 @@ protected function _snakeYaml() * * @return bool True if the naming strategy is upper camel case; otherwise, false */ - protected function isUpperCamel() + protected function _upperCamel() { return isset($this->_classParams[self::JSON]) && isset($this->_classParams[self::JSON][self::PROPERTY_NAMING_STRATEGY]) @@ -1516,13 +1521,27 @@ protected function _pretty() ; } + /** + * Checks if the provided parameter is an array. + * + * This function verifies if the given parameter is set and is of type array. It is a helper method + * used to validate the type of data before performing any operations on it that require an array. + * + * @param mixed $params The parameter to check. + * @return bool Returns `true` if the parameter is set and is an array, otherwise returns `false`. + */ + private function _isArray($params) + { + return isset($params) && is_array($params); + } + /** * Check if a value is not null and not empty * * @param mixed $value The value to check * @return bool True if the value is not null and not empty; otherwise, false */ - private function _notNullAndNotEmpty($value) + private function _isNotNullAndNotEmpty($value) { return $value != null && !empty($value); } @@ -1582,16 +1601,6 @@ public function listAll($specification = null, $pageable = null, $sortable = nul return $this->findAll($specification, $pageable, $sortable, $passive, $subqueryMap); } - /** - * Check if database is connected or not - * - * @return bool - */ - private function _databaseConnected() - { - return $this->_database != null && $this->_database->isConnected(); - } - /** * Count the data based on specifications * @@ -1688,9 +1697,9 @@ public function findAll($specification = null, $pageable = null, $sortable = nul $startTime = microtime(true); try { - $pageData = new PicoPageData(array(), $startTime); if($this->_databaseConnected()) { + $pageData = new PicoPageData(array(), $startTime); $persist = new PicoDatabasePersistence($this->_database, $this); if($findOption & self::FIND_OPTION_NO_FETCH_DATA) { @@ -1778,10 +1787,10 @@ public function findSpecific($selected, $specification = null, $pageable = null, { $startTime = microtime(true); try - { - $pageData = new PicoPageData(array(), $startTime); + { if($this->_databaseConnected()) { + $pageData = new PicoPageData(array(), $startTime); $persist = new PicoDatabasePersistence($this->_database, $this); if($findOption & self::FIND_OPTION_NO_FETCH_DATA) { @@ -1908,7 +1917,7 @@ public function find($params) { $persist = new PicoDatabasePersistence($this->_database, $this); $result = $persist->find($params); - if($this->_notNullAndNotEmpty($result)) + if($this->_isNotNullAndNotEmpty($result)) { $this->loadData($result); return $this; @@ -2054,7 +2063,7 @@ public function findOneWithPrimaryKeyValue($primaryKeyVal, $subqueryMap = null) { $persist = new PicoDatabasePersistence($this->_database, $this); $result = $persist->findOneWithPrimaryKeyValue($primaryKeyVal, $subqueryMap); - if($this->_notNullAndNotEmpty($result)) + if($this->_isNotNullAndNotEmpty($result)) { $this->loadData($result); return $this; @@ -2086,7 +2095,7 @@ private function findOneBy($method, $params, $sortable = null) { $persist = new PicoDatabasePersistence($this->_database, $this); $result = $persist->findOneBy($method, $params, $sortable); - if($this->_notNullAndNotEmpty($result)) + if($this->_isNotNullAndNotEmpty($result)) { $this->loadData($result); return $this; @@ -2209,11 +2218,11 @@ private function booleanToTextBy($propertyName, $params) * @param bool $passive Flag indicating whether the objects are passive. * @return array An array of objects. */ - private function toArrayObject($result, $passive = false) // NOSONAR + private function toArrayObject($result, $passive = false) { $instance = array(); $index = 0; - if(isset($result) && is_array($result)) + if($this->_isArray($result)) { foreach($result as $value) { @@ -2404,7 +2413,7 @@ public function __call($method, $params) // NOSONAR $var = lcfirst(substr($method, 3)); return isset($this->{$var}) ? $this->{$var} : null; } - else if (strncasecmp($method, "set", 3) === 0 && isset($params) && is_array($params) && !empty($params) && !$this->_readonly) { + else if (strncasecmp($method, "set", 3) === 0 && $this->_isArray($params) && !empty($params) && !$this->_readonly) { $var = lcfirst(substr($method, 3)); $this->{$var} = $params[0]; $this->modifyNullProperties($var, $params[0]); @@ -2415,21 +2424,21 @@ public function __call($method, $params) // NOSONAR $this->removeValue($var); return $this; } - else if (strncasecmp($method, "push", 4) === 0 && isset($params) && is_array($params) && !$this->_readonly) { + else if (strncasecmp($method, "push", 4) === 0 && $this->_isArray($params) && !$this->_readonly) { $var = lcfirst(substr($method, 4)); - return $this->push($var, isset($params) && is_array($params) && isset($params[0]) ? $params[0] : null); + return $this->push($var, $this->_isArray($params) && isset($params[0]) ? $params[0] : null); } - else if (strncasecmp($method, "append", 6) === 0 && isset($params) && is_array($params) && !$this->_readonly) { + else if (strncasecmp($method, "append", 6) === 0 && $this->_isArray($params) && !$this->_readonly) { $var = lcfirst(substr($method, 6)); - return $this->append($var, isset($params) && is_array($params) && isset($params[0]) ? $params[0] : null); + return $this->append($var, $this->_isArray($params) && isset($params[0]) ? $params[0] : null); } - else if (strncasecmp($method, "unshift", 7) === 0 && isset($params) && is_array($params) && !$this->_readonly) { + else if (strncasecmp($method, "unshift", 7) === 0 && $this->_isArray($params) && !$this->_readonly) { $var = lcfirst(substr($method, 7)); - return $this->unshift($var, isset($params) && is_array($params) && isset($params[0]) ? $params[0] : null); + return $this->unshift($var, $this->_isArray($params) && isset($params[0]) ? $params[0] : null); } - else if (strncasecmp($method, "prepend", 7) === 0 && isset($params) && is_array($params) && !$this->_readonly) { + else if (strncasecmp($method, "prepend", 7) === 0 && $this->_isArray($params) && !$this->_readonly) { $var = lcfirst(substr($method, 7)); - return $this->prepend($var, isset($params) && is_array($params) && isset($params[0]) ? $params[0] : null); + return $this->prepend($var, $this->_isArray($params) && isset($params[0]) ? $params[0] : null); } else if (strncasecmp($method, "pop", 3) === 0) { $var = lcfirst(substr($method, 3)); @@ -2660,7 +2669,7 @@ public function __toString() $obj->set($key, $value); } } - $upperCamel = $this->isUpperCamel(); + $upperCamel = $this->_upperCamel(); if($upperCamel) { $value = $this->valueArrayUpperCamel(); diff --git a/src/Request/InputCookie.php b/src/Request/InputCookie.php index 5d283feb..724280b3 100644 --- a/src/Request/InputCookie.php +++ b/src/Request/InputCookie.php @@ -6,6 +6,8 @@ /** * Class for handling input from cookies. + * This class provides functionality to retrieve and parse data from the global $_COOKIE array. + * It supports options for recursively converting objects and parsing specific types like null and boolean values. * * @author Kamshory * @package MagicObject\Request @@ -16,16 +18,18 @@ class InputCookie extends PicoRequestBase { /** * Indicates whether to recursively convert all objects. * - * @var boolean + * @var bool */ private $_recursive = false; // NOSONAR /** * Constructor for the InputCookie class. + * Initializes the InputCookie instance, optionally setting flags for recursive object conversion, + * parsing null and boolean values, and forcing scalar value retrieval. * - * @param bool $recursive Flag to indicate if all objects should be converted recursively. - * @param bool $parseNullAndBool Flag to indicate whether to parse NULL and BOOL values. - * @param bool $forceScalar Flag to indicate if only scalar values should be retrieved. + * @param bool $recursive Flag to indicate if all objects should be converted recursively (default is false). + * @param bool $parseNullAndBool Flag to indicate whether to parse NULL and BOOL values from cookies (default is false). + * @param bool $forceScalar Flag to indicate if only scalar values should be retrieved (default is false). */ public function __construct($recursive = false, $parseNullAndBool = false, $forceScalar = false) { @@ -40,9 +44,11 @@ public function __construct($recursive = false, $parseNullAndBool = false, $forc } /** - * Get the global variable $_COOKIE. + * Get the global $_COOKIE variable. + * + * This method is a static wrapper to return the raw cookie data from the $_COOKIE superglobal. * - * @return array The cookie data. + * @return array The cookie data from the $_COOKIE superglobal. */ public static function requestCookie() { @@ -50,15 +56,18 @@ public static function requestCookie() } /** - * Override the loadData method to load cookie data. + * Load cookie data into the object. + * This method populates the object's properties with data from the provided cookie array. + * It supports recursive object parsing if the _recursive flag is set. * - * @param array $data Data to load into the object. + * @param array $data Data to load into the object (usually from $_COOKIE). * @param bool $tolower Flag to indicate if the keys should be converted to lowercase (default is false). * @return self Returns the current instance for method chaining. */ public function loadData($data, $tolower = false) { if ($this->_recursive) { + // Parse the data recursively if the recursive flag is set. $genericObject = PicoObjectParser::parseJsonRecursive($data); if ($genericObject !== null) { $values = $genericObject->valueArray(); @@ -70,6 +79,7 @@ public function loadData($data, $tolower = false) } } } else { + // Load data without recursion. parent::loadData($data); } return $this; diff --git a/src/Request/InputServer.php b/src/Request/InputServer.php index ffadf70d..d47aed30 100644 --- a/src/Request/InputServer.php +++ b/src/Request/InputServer.php @@ -10,34 +10,34 @@ * address, and script name. * * Available methods: - * - getPhpSelf() returns $_SERVER['PHP_SELF'] - * - getGatewayInterface() returns $_SERVER['GATEWAY_INTERFACE'] - * - getServerAddr() returns $_SERVER['SERVER_ADDR'] - * - getScriptName() returns $_SERVER['SCRIPT_NAME'] - * - getServerSoftware() returns $_SERVER['SERVER_SOFTWARE'] - * - getServerProtocol() returns $_SERVER['SERVER_PROTOCOL'] - * - getRequestMethod() returns $_SERVER['REQUEST_METHOD'] - * - getRequestTime() returns $_SERVER['REQUEST_TIME'] - * - getRequestTimeFloat() returns $_SERVER['REQUEST_TIME_FLOAT'] - * - getQueryString() returns $_SERVER['QUERY_STRING'] - * - getDocumentRoot() returns $_SERVER['DOCUMENT_ROOT'] - * - getHttps() returns $_SERVER['HTTPS'] - * - getRemoteAddr() returns $_SERVER['REMOTE_ADDR'] - * - getRemotePort() returns $_SERVER['REMOTE_PORT'] - * - getRemoteUser() returns $_SERVER['REMOTE_USER'] - * - getRedirectRemoteUser() returns $_SERVER['REDIRECT_REMOTE_USER'] - * - getScriptFilename() returns $_SERVER['SCRIPT_FILENAME'] - * - getServerAdmin() returns $_SERVER['SERVER_ADMIN'] - * - getServerPort() returns $_SERVER['SERVER_PORT'] - * - getServerSignature() returns $_SERVER['SERVER_SIGNATURE'] - * - getPathTranslated() returns $_SERVER['PATH_TRANSLATED'] - * - getRequestUri() returns $_SERVER['REQUEST_URI'] - * - getPhpAuthDigest() returns $_SERVER['PHP_AUTH_DIGEST'] - * - getPhpAuthUser() returns $_SERVER['PHP_AUTH_USER'] - * - getPhpAuthPw() returns $_SERVER['PHP_AUTH_PW'] - * - getAuthType() returns $_SERVER['AUTH_TYPE'] - * - getPathInfo() returns $_SERVER['PATH_INFO'] - * - getOrigPathInfo() returns $_SERVER['ORIG_PATH_INFO'] + * - `getPhpSelf()` returns `$_SERVER['PHP_SELF']` + * - `getGatewayInterface()` returns `$_SERVER['GATEWAY_INTERFACE']` + * - `getServerAddr()` returns `$_SERVER['SERVER_ADDR']` + * - `getScriptName()` returns `$_SERVER['SCRIPT_NAME']` + * - `getServerSoftware()` returns `$_SERVER['SERVER_SOFTWARE']` + * - `getServerProtocol()` returns `$_SERVER['SERVER_PROTOCOL']` + * - `getRequestMethod()` returns `$_SERVER['REQUEST_METHOD']` + * - `getRequestTime()` returns `$_SERVER['REQUEST_TIME']` + * - `getRequestTimeFloat()` returns `$_SERVER['REQUEST_TIME_FLOAT']` + * - `getQueryString()` returns `$_SERVER['QUERY_STRING']` + * - `getDocumentRoot()` returns `$_SERVER['DOCUMENT_ROOT']` + * - `getHttps()` returns `$_SERVER['HTTPS']` + * - `getRemoteAddr()` returns `$_SERVER['REMOTE_ADDR']` + * - `getRemotePort()` returns `$_SERVER['REMOTE_PORT']` + * - `getRemoteUser()` returns `$_SERVER['REMOTE_USER']` + * - `getRedirectRemoteUser()` returns `$_SERVER['REDIRECT_REMOTE_USER']` + * - `getScriptFilename()` returns `$_SERVER['SCRIPT_FILENAME']` + * - `getServerAdmin()` returns `$_SERVER['SERVER_ADMIN']` + * - `getServerPort()` returns `$_SERVER['SERVER_PORT']` + * - `getServerSignature()` returns `$_SERVER['SERVER_SIGNATURE']` + * - `getPathTranslated()` returns `$_SERVER['PATH_TRANSLATED']` + * - `getRequestUri()` returns `$_SERVER['REQUEST_URI']` + * - `getPhpAuthDigest()` returns `$_SERVER['PHP_AUTH_DIGEST']` + * - `getPhpAuthUser()` returns `$_SERVER['PHP_AUTH_USER']` + * - `getPhpAuthPw()` returns `$_SERVER['PHP_AUTH_PW']` + * - `getAuthType()` returns `$_SERVER['AUTH_TYPE']` + * - `getPathInfo()` returns `$_SERVER['PATH_INFO']` + * - `getOrigPathInfo()` returns `$_SERVER['ORIG_PATH_INFO']` * * @author Kamshory * @package MagicObject\Request diff --git a/src/Request/PicoRequestBase.php b/src/Request/PicoRequestBase.php index 30deac0d..d9272472 100644 --- a/src/Request/PicoRequestBase.php +++ b/src/Request/PicoRequestBase.php @@ -9,9 +9,11 @@ use ReflectionClass; use stdClass; + /** - * Base class for handling requests. - * + * Base class for handling HTTP requests, including input sanitization, data manipulation, + * and request type checking (GET, POST, AJAX, etc.). + * * @author Kamshory * @package MagicObject\Database * @link https://github.com/Planetbiru/Request @@ -19,30 +21,31 @@ class PicoRequestBase extends stdClass //NOSONAR { /** - * Class parameters. + * Class parameters parsed from annotations. * * @var array */ private $classParams = array(); /** - * Flag to force input object as scalar. + * Flag to force input data to be scalar only. * - * @var boolean + * @var bool */ protected $forceScalar = false; /** - * Flag for recursive processing. + * Flag for recursive data processing. * - * @var boolean + * @var bool */ protected $_recursive = false; /** - * Constructor + * Constructor to initialize the request handler and process class annotations. * - * @param bool $forceScalar Indicates whether to only accept scalar values. + * @param bool $forceScalar Indicates whether to accept only scalar values for data input. + * @throws InvalidAnnotationException If there are invalid annotations in the class. */ public function __construct($forceScalar = false) { @@ -61,10 +64,10 @@ public function __construct($forceScalar = false) } /** - * Load data into the object. + * Load data into the object, transforming keys to camelCase (optional). * - * @param mixed $data Data to be loaded. - * @param bool $tolower Flag to transform keys to lowercase. + * @param mixed $data Data to be loaded (can be an array or object). + * @param bool $tolower Flag indicating whether to convert keys to lowercase before loading. */ public function loadData($data, $tolower = false) { @@ -81,10 +84,10 @@ public function loadData($data, $tolower = false) } /** - * Set the value of a property. + * Set a property value dynamically on the object using camelCase notation. * - * @param string $propertyName Name of the property. - * @param mixed|null $propertyValue Value of the property. + * @param string $propertyName Name of the property to set. + * @param mixed $propertyValue Value to assign to the property. * @return self */ public function set($propertyName, $propertyValue) @@ -95,10 +98,10 @@ public function set($propertyName, $propertyValue) } /** - * Get the value of a property. + * Get a property value dynamically from the object. * - * @param string $propertyName Name of the property. - * @param array|null $params Parameters for filtering. + * @param string $propertyName Name of the property to retrieve. + * @param array|null $params Optional parameters for filtering the value. * @return mixed|null */ public function get($propertyName, $params = null) @@ -129,9 +132,9 @@ public function get($propertyName, $params = null) } /** - * Get the value of the object. + * Get the values of all properties as an object (optionally in snake_case). * - * @param bool $snakeCase Flag to define naming strategy. + * @param bool $snakeCase Flag to convert property names to snake_case. * @return stdClass */ public function value($snakeCase = false) @@ -159,10 +162,10 @@ public function value($snakeCase = false) } /** - * Get a list of properties. + * Retrieve a list of properties defined in the class, optionally as an array of property names. * - * @param bool $reflectSelf Flag to indicate if class reflection should be used. - * @param bool $asArrayProps Flag to return properties as an array. + * @param bool $reflectSelf Flag to indicate whether to include only properties of the current class (not inherited). + * @param bool $asArrayProps Flag to return properties as an array of names. * @return array */ protected function propertyList($reflectSelf = false, $asArrayProps = false) @@ -196,12 +199,12 @@ function($property) use($class) } /** - * Filter input data. + * Filter input data from global variables (GET, POST, etc.) according to the specified filter type. * - * @param int $type Request type. - * @param string $variableName Name of the variable. - * @param int $filter Filter type. - * @param bool $escapeSQL Flag to escape SQL. + * @param int $type The type of input (e.g., INPUT_GET, INPUT_POST). + * @param string $variableName The name of the variable to filter. + * @param int $filter The filter type to apply (e.g., FILTER_SANITIZE_EMAIL). + * @param bool $escapeSQL Flag to escape SQL-specific characters. * @return mixed */ public function filterInput($type, $variableName, $filter = PicoFilterConstant::FILTER_DEFAULT, $escapeSQL=false) // NOSONAR @@ -230,13 +233,13 @@ public function filterInput($type, $variableName, $filter = PicoFilterConstant:: } /** - * Filter a value based on the specified criteria. + * Filter a value (or nested values) based on the specified filter type and optional flags. * * @param mixed $val The value to be filtered. - * @param int $filter The filter type. - * @param bool $escapeSQL Flag to escape SQL. + * @param int $filter The filter type to apply (e.g., FILTER_SANITIZE_URL). + * @param bool $escapeSQL Flag to escape SQL-specific characters. * @param bool $nullIfEmpty Flag to return null if the value is empty. - * @param bool $requireScalar Flag to require scalar values only. + * @param bool $requireScalar Flag to require scalar values. * @return mixed|null */ public function filterValue($val, $filter = PicoFilterConstant::FILTER_DEFAULT, $escapeSQL = false, $nullIfEmpty = false, $requireScalar = false) @@ -273,11 +276,11 @@ public function filterValue($val, $filter = PicoFilterConstant::FILTER_DEFAULT, } /** - * Filter a single value based on the specified criteria. + * Filter a single value based on the specified filter type, applying specific sanitization rules. * * @param mixed $val The value to be filtered. - * @param int $filter The filter type. - * @param bool $escapeSQL Flag to escape SQL. + * @param int $filter The filter type to apply (e.g., FILTER_SANITIZE_NUMBER_INT). + * @param bool $escapeSQL Flag to escape SQL-specific characters. * @param bool $nullIfEmpty Flag to return null if the value is empty. * @return mixed */ @@ -419,9 +422,9 @@ public function filterValueSingle($val, $filter = PicoFilterConstant::FILTER_DEF } /** - * Add slashes to a string. + * Add escape slashes to a string to protect against SQL injection or special character issues. * - * @param string $input The input value. + * @param string $input The input string to escape. * @return string */ public function addslashes($input) @@ -430,10 +433,11 @@ public function addslashes($input) } /** - * Get the value from a formatted number. + * Format and return a numeric value by considering application-specific settings for decimal + * and thousand separators. * - * @param stdClass|MagicObject $cfg Configuration object. - * @param mixed $input Input value. + * @param stdClass|MagicObject $cfg Configuration object containing separators. + * @param mixed $input The input value to format. * @return float */ public function _getValue($cfg, $input) @@ -468,7 +472,7 @@ public function _getValue($cfg, $input) /** * Check if the request is a GET request. * - * @return bool + * @return bool True if the request method is GET, false otherwise. */ public function isGet() { @@ -478,7 +482,7 @@ public function isGet() /** * Check if the request is a POST request. * - * @return bool + * @return bool True if the request method is POST, false otherwise. */ public function isPost() { @@ -488,7 +492,7 @@ public function isPost() /** * Check if the request is an AJAX request. * - * @return bool + * @return bool True if the request is an AJAX request, false otherwise. */ public function isAjax() { @@ -496,9 +500,9 @@ public function isAjax() } /** - * Retrieve the HTTP method used for the request. + * Retrieve the HTTP method used for the current request. * - * @return string + * @return string The HTTP method (e.g., GET, POST). */ public function getHttpMethod() { @@ -506,9 +510,9 @@ public function getHttpMethod() } /** - * Retrieve the user agent of the request. + * Retrieve the user agent string from the request headers. * - * @return string + * @return string The user agent string. */ public function getUserAgent() { @@ -516,9 +520,9 @@ public function getUserAgent() } /** - * Retrieve the client IP address. + * Retrieve the client's IP address from the request headers. * - * @return string + * @return string The client's IP address. */ public function getClientIp() { diff --git a/src/SecretObject.php b/src/SecretObject.php index e32281b3..f02d9e6c 100644 --- a/src/SecretObject.php +++ b/src/SecretObject.php @@ -18,12 +18,24 @@ use Symfony\Component\Yaml\Yaml; /** - * Secret object - * - * This class provides mechanisms for managing properties with encryption - * and decryption capabilities, using annotations to specify which properties - * should be secured. - * + * SecretObject class + * + * This class provides mechanisms to manage properties that require encryption + * and decryption during their lifecycle. It uses annotations to specify which + * properties should be encrypted or decrypted when they are set or retrieved. + * These annotations help identify when to apply encryption or decryption, + * either before saving (SET) or before fetching (GET). + * + * The class supports flexibility in data initialization, allowing data to be + * passed as an array, an object, or even left empty. Additionally, a secure + * callback function can be provided to handle key generation for encryption + * and decryption operations. + * + * Key features: + * - Encryption and decryption of object properties based on annotations. + * - Support for customizing property naming strategies. + * - Option to provide a secure function for key generation. + * * @author Kamshory * @package MagicObject * @link https://github.com/Planetbiru/MagicObject @@ -129,17 +141,19 @@ public function __construct($data = null, $secureCallback = null) } /** - * Processes object information to determine encryption and decryption requirements. + * Analyzes the class's parameters and properties to determine which should be + * encrypted or decrypted based on annotations. * - * This method retrieves the class parameters and properties using reflection, - * parsing annotations to identify which properties should be encrypted or - * decrypted. It populates the respective lists of properties based on the - * annotations found. + * This method uses reflection to retrieve the class's parameters and properties. + * It then parses annotations associated with these members to identify which + * properties should undergo encryption or decryption during specific stages + * (before storage or before retrieval). The appropriate lists of properties + * are populated accordingly. * * @return void * * @throws InvalidAnnotationException If an invalid annotation is encountered - * while processing class parameters. + * while processing class parameters or properties. */ private function _objectInfo() { @@ -148,42 +162,34 @@ private function _objectInfo() $params = $reflexClass->getParameters(); $props = $reflexClass->getProperties(); - foreach($params as $paramName=>$paramValue) - { - try - { + // Process each class parameter + foreach ($params as $paramName => $paramValue) { + try { $vals = $reflexClass->parseKeyValue($paramValue); $this->_classParams[$paramName] = $vals; - } - catch(InvalidQueryInputException $e) - { - throw new InvalidAnnotationException("Invalid annotation @".$paramName); + } catch (InvalidQueryInputException $e) { + throw new InvalidAnnotationException("Invalid annotation @" . $paramName); } } - // iterate each properties of the class - foreach($props as $prop) - { + // Process each class property + foreach ($props as $prop) { $reflexProp = new PicoAnnotationParser($className, $prop->name, 'property'); $parameters = $reflexProp->getParameters(); - // add property list to be encryped or decrypted - foreach($parameters as $param=>$val) - { - if(strcasecmp($param, self::ANNOTATION_ENCRYPT_IN) == 0) - { + // Check each property for encryption/decryption annotations + foreach ($parameters as $param => $val) { + if (strcasecmp($param, self::ANNOTATION_ENCRYPT_IN) == 0) { + // Property should be encrypted before storing $this->_encryptInProperties[] = $prop->name; - } - else if(strcasecmp($param, self::ANNOTATION_DECRYPT_OUT) == 0) - { + } else if (strcasecmp($param, self::ANNOTATION_DECRYPT_OUT) == 0) { + // Property should be decrypted before retrieval $this->_decryptOutProperties[] = $prop->name; - } - else if(strcasecmp($param, self::ANNOTATION_ENCRYPT_OUT) == 0) - { + } else if (strcasecmp($param, self::ANNOTATION_ENCRYPT_OUT) == 0) { + // Property should be encrypted before retrieval $this->_encryptOutProperties[] = $prop->name; - } - else if(strcasecmp($param, self::ANNOTATION_DECRYPT_IN) == 0) - { + } else if (strcasecmp($param, self::ANNOTATION_DECRYPT_IN) == 0) { + // Property should be decrypted before storing $this->_decryptInProperties[] = $prop->name; } } diff --git a/src/Util/Database/EntityUtil.php b/src/Util/Database/EntityUtil.php index d92833c0..44b974c7 100644 --- a/src/Util/Database/EntityUtil.php +++ b/src/Util/Database/EntityUtil.php @@ -29,10 +29,10 @@ public static function getPropertyColumn($entity) $tableInfo = $entity->tableInfo(); if($tableInfo == null) { - return array(); + return []; } $columns = $tableInfo->getColumns(); - $propertyColumns = array(); + $propertyColumns = []; foreach($columns as $prop=>$column) { $propertyColumns[$prop] = $column['name']; @@ -51,10 +51,10 @@ public static function getPropertyJoinColumn($entity) $tableInfo = $entity->tableInfo(); if($tableInfo == null) { - return array(); + return []; } $joinColumns = $tableInfo->getJoinColumns(); - $propertyColumns = array(); + $propertyColumns = []; foreach($joinColumns as $prop=>$column) { $propertyColumns[$prop] = $column['name']; @@ -71,7 +71,7 @@ public static function getPropertyJoinColumn($entity) */ public static function getEntityData($data, $map) { - $newData = array(); + $newData = []; if(isset($data)) { if(is_array($data)) @@ -99,7 +99,7 @@ public static function getEntityData($data, $map) */ private static function fromArray($data, $map) { - $newData = array(); + $newData = []; foreach($map as $key=>$value) { if(isset($data[$value])) @@ -119,7 +119,7 @@ private static function fromArray($data, $map) */ private static function fromStdClass($data, $map) { - $newData = array(); + $newData = []; foreach($map as $key=>$value) { if(isset($data->{$value})) @@ -139,7 +139,7 @@ private static function fromStdClass($data, $map) */ private static function fromMagicObject($data, $map) { - $newData = array(); + $newData = []; foreach($map as $key=>$value) { $newData[$key] = $data->get($value); diff --git a/src/Util/Database/NativeQueryUtil.php b/src/Util/Database/NativeQueryUtil.php new file mode 100644 index 00000000..4e12d284 --- /dev/null +++ b/src/Util/Database/NativeQueryUtil.php @@ -0,0 +1,357 @@ + $paramValue) { + if($paramValue instanceof PicoPageable) + { + $pageable = $paramValue; + } + else if($paramValue instanceof PicoSortable) + { + $sortable = $paramValue; + } + else if (isset($callerParams[$index])) { + // Format parameter name according to the query + $paramName = $callerParams[$index]->getName(); + if(is_array($paramValue)) + { + $queryString = str_replace(":".$paramName, PicoDatabaseUtil::toList($paramValue, true, true), $queryString); + } + } + } + + // Apply pagination and sorting if needed + if(isset($pageable) || isset($sortable)) + { + $queryBuilder = new PicoDatabaseQueryBuilder($databaseType); + $queryString = $queryBuilder->addPaginationAndSorting($queryString, $pageable, $sortable); + } + + return $queryString; + } + + /** + * Handles the return of data based on the specified return type. + * + * This method processes the data returned from a PDO statement and returns it in the format + * specified by the caller's `@return` docblock annotation. It supports various return types + * such as `void`, `PDOStatement`, `int`, `object`, `array`, `string`, or any specific class + * name (including array-type hinting). + * + * @param PDOStatement $stmt The executed PDO statement. + * @param string $returnType The return type as specified in the caller function's docblock. + * @return mixed The processed return data, which can be a single value, object, array, + * PDOStatement, or a JSON string, based on the return type. + * @throws InvalidReturnTypeException If the return type is invalid or unrecognized. + */ + public function handleReturnObject($stmt, $returnType) //NOSONAR + { + // Handle basic return types + switch ($returnType) { + case 'void': + return null; + + case 'PDOStatement': + return $stmt; + + case 'int': + case 'integer': + return $stmt->rowCount(); + + case 'object': + case 'stdClass': + return $stmt->fetch(PDO::FETCH_OBJ); + + case 'array': + return $stmt->fetchAll(PDO::FETCH_ASSOC); + + case 'string': + return json_encode($stmt->fetchAll(PDO::FETCH_OBJ)); + default: + break; + } + + // Handle array-type hinting (e.g., MagicObject[], MyClass[]) + if (strpos($returnType, "[") !== false) { + return $this->handleArrayReturnType($stmt, $returnType); + } + + // Handle single class-type return (e.g., MagicObject, MyClass) + return $this->handleSingleClassReturnType($stmt, $returnType); + } + + /** + * Handles return types with array hinting (e.g., `MagicObject[]`, `MyClass[]`). + * + * @param PDOStatement $stmt The executed PDO statement. + * @param string $returnType The array-type return type (e.g., `MagicObject[]`). + * @return array The processed result as an array of objects. + * @throws InvalidReturnTypeException If the return type is invalid or unrecognized. + */ + private function handleArrayReturnType($stmt, $returnType) + { + $className = trim(explode("[", $returnType)[0]); + + if ($className === 'stdClass') { + return $stmt->fetchAll(PDO::FETCH_OBJ); + } elseif ($className === 'MagicObject') { + return $this->mapRowsToMagicObject($stmt); + } elseif (class_exists($className)) { + return $this->mapRowsToClass($stmt, $className); + } else { + throw new InvalidReturnTypeException("Invalid return type for array of $className"); + } + } + + /** + * Handles return types that are a single object (e.g., `MagicObject`, `MyClass`). + * + * @param PDOStatement $stmt The executed PDO statement. + * @param string $returnType The single-class return type (e.g., `MagicObject`). + * @return mixed The processed result as a single object. + * @throws InvalidReturnTypeException If the return type is invalid or unrecognized. + */ + private function handleSingleClassReturnType($stmt, $returnType) + { + $className = trim($returnType); + + // Check if the return type is 'MagicObject' + if ($className === 'MagicObject') { + $row = $stmt->fetch(PDO::FETCH_OBJ); + return new MagicObject($row); + } + + // Check if the class exists + if (class_exists($className)) { + $obj = new $className(); + + // If the class is an instance of MagicObject, load data + if ($obj instanceof MagicObject) { + $row = $stmt->fetch(PDO::FETCH_OBJ); + return $obj->loadData($row); + } + + // Return the class instance (assuming it's valid) + return $obj; + } + + // Throw an exception if the class does not exist or the return type is invalid + throw new InvalidReturnTypeException("Invalid return type for $className"); + } + + /** + * Maps rows from the PDO statement to an array of MagicObject instances. + * + * @param PDOStatement $stmt The executed PDO statement. + * @return MagicObject[] An array of MagicObject instances. + */ + private function mapRowsToMagicObject($stmt) + { + $result = $stmt->fetchAll(PDO::FETCH_OBJ); + $objects = []; + + foreach ($result as $row) { + $objects[] = new MagicObject($row); + } + + return $objects; + } + + /** + * Maps rows from the PDO statement to an array of instances of a specified class. + * + * @param PDOStatement $stmt The executed PDO statement. + * @param string $className The class name to map rows to. + * @return object[] An array of instances of the specified class. + * @throws InvalidReturnTypeException If the class does not exist. + */ + private function mapRowsToClass($stmt, $className) + { + $result = $stmt->fetchAll(PDO::FETCH_OBJ); + $objects = []; + + foreach ($result as $row) { + $objects[] = new $className($row); + } + + return $objects; + } + + /** + * Extracts the return type from the docblock of the caller function. + * + * The method processes the `@return` annotation in the docblock of the caller function, + * and adjusts for `self` to return the actual caller class name. It also handles array type + * return values. + * + * @param string $docComment The docblock comment of the caller function. + * @param string $callerClassName The name of the class where the caller function is defined. + * @return string The processed return type, which could be a class name, `self`, or `void`. + */ + public function extractReturnType($docComment, $callerClassName) + { + // Get return type from the caller function + preg_match('/@return\s+([^\s]+)/', $docComment, $matches); + $returnType = $matches ? $matches[1] : 'void'; + + // Trim return type + $returnType = trim($returnType); + + // Change self to callerClassName + if ($returnType == "self[]") { + $returnType = $callerClassName . "[]"; + } else if ($returnType == "self") { + $returnType = $callerClassName; + } + + return $returnType; + } + + /** + * Extracts the query string from the docblock of the caller function. + * + * The method looks for the `@query` annotation in the docblock and extracts the query string. + * It tries to handle different formats for the annotation, throwing an exception if no query is found. + * + * @param string $docComment The docblock comment of the caller function. + * @return string The SQL query string extracted from the `@query` annotation. + * @throws InvalidQueryInputException If no query string is found in the docblock. + */ + public function extractQueryString($docComment) + { + // Get the query from the @query annotation + preg_match('/@query\s*\("([^"]+)"\)/', $docComment, $matches); + $queryString = $matches ? $matches[1] : ''; + + // Trim the query string of whitespace and line breaks + $queryString = trim($queryString, " \r\n\t "); + + if (empty($queryString)) { + // Try reading the query in another way + preg_match('/@query\s*\(\s*"(.*?)"\s*\)/s', $docComment, $matches); + $queryString = $matches ? $matches[1] : ''; + + if (empty($queryString)) { + throw new InvalidQueryInputException("No query found.\r\n" . $docComment); + } + } + + return $queryString; + } + + + /** + * Maps PHP types to PDO parameter types. + * + * This function determines the appropriate PDO parameter type based on the given value. + * It handles various PHP data types and converts them to the corresponding PDO parameter types + * required for executing prepared statements in PDO. + * + * @param mixed $value The value to determine the type for. This can be of any type, including + * null, boolean, integer, string, DateTime, or other types. + * @return stdClass An object containing: + * - type: The PDO parameter type (PDO::PARAM_STR, PDO::PARAM_NULL, + * PDO::PARAM_BOOL, PDO::PARAM_INT). + * - value: The corresponding value formatted as needed for the PDO parameter. + */ + public function mapToPdoParamType($value) + { + $type = PDO::PARAM_STR; // Default type is string + $finalValue = $value; // Initialize final value to the original value + + if ($value instanceof DateTime) { + $type = PDO::PARAM_STR; // DateTime should be treated as a string + $finalValue = $value->format("Y-m-d H:i:s"); + } else if (is_null($value)) { + $type = PDO::PARAM_NULL; // NULL type + $finalValue = null; // Set final value to null + } else if (is_bool($value)) { + $type = PDO::PARAM_BOOL; // Boolean type + $finalValue = $value; // Keep the boolean value + } else if (is_int($value)) { + $type = PDO::PARAM_INT; // Integer type + $finalValue = $value; // Keep the integer value + } + + // Create and return an object with the type and value + $result = new stdClass(); + $result->type = $type; + $result->value = $finalValue; + return $result; + } + + /** + * Debugs an SQL query by sending it to a logger callback function. + * + * This method retrieves the callback function for debugging queries from the database object + * and invokes it with the final SQL query string, which is generated by combining the SQL + * statement with the provided parameters. + * + * @param PicoDatabase $database The database instance that contains the callback function. + * @param PDOStatement $stmt The PDO statement object that holds the prepared query. + * @param array $params The parameters that are bound to the SQL statement. + * + * @return void + */ + public function debugQuery($database, $stmt, $params) + { + // Send query to logger + $debugFunction = $database->getCallbackDebugQuery(); + if (isset($debugFunction) && is_callable($debugFunction)) { + call_user_func($debugFunction, PicoDatabaseUtil::getFinalQuery($stmt, $params)); + } + } + +} \ No newline at end of file diff --git a/src/Util/Database/PicoDatabaseUtil.php b/src/Util/Database/PicoDatabaseUtil.php index a5d793db..7eac8495 100644 --- a/src/Util/Database/PicoDatabaseUtil.php +++ b/src/Util/Database/PicoDatabaseUtil.php @@ -109,7 +109,7 @@ public static function sortableFromParams($params) */ public static function valuesFromParams($params) { - $ret = array(); + $ret = []; if(self::isArray($params)) { foreach($params as $param) @@ -352,100 +352,65 @@ public static function uuid() * @param string $sqlText The raw SQL string containing one or more queries. * @return array An array of queries with their respective delimiters. */ - public function splitSql($sqlText) //NOSONAR + public function splitSql($sqlText) { - $sqlText = str_replace("\n", "\r\n", $sqlText); - $sqlText = str_replace("\r\r\n", "\r\n", $sqlText); - $arr = explode("\r\n", $sqlText); - $arr2 = array(); - foreach($arr as $key=>$val) - { - $arr[$key] = ltrim($val); - if(stripos($arr[$key], "-- ") !== 0 && $arr[$key] != "--" && $arr[$key] != "") - { - $arr2[] = $arr[$key]; - } - } - $arr = $arr2; - unset($arr2); + // Normalize newlines and clean up any redundant line breaks + $sqlText = str_replace("\r\r\n", "\r\n", str_replace("\n", "\r\n", $sqlText)); + + // Split the SQL text by newlines + $lines = explode("\r\n", $sqlText); + + // Clean up lines, remove comments, and empty lines + $cleanedLines = array_filter(array_map('ltrim', $lines), function ($line) { + return !(empty($line) || stripos($line, "-- ") === 0 || $line == "--"); + }); + + // Initialize state variables + $queries = []; + $currentQuery = ''; + $isAppending = false; + $delimiter = ';'; + $skip = false; - $append = 0; - $skip = 0; - $start = 1; - $nquery = -1; - $delimiter = ";"; - $queryArray = array(); - $delimiterArray = array(); + foreach ($cleanedLines as $line) { + // Skip lines if needed + if ($skip) { + $skip = false; + continue; + } - foreach($arr as $line=>$text) - { - if($text == "" && $append == 1) - { - $queryArray[$nquery] .= "\r\n"; + // Handle "delimiter" statements + if (stripos(trim($line), 'delimiter ') === 0) { + $parts = explode(' ', trim($line)); + $delimiter = $parts[1] ?? ';'; + continue; } - if($append == 0) - { - if(stripos(ltrim($text, " \t "), "--") === 0) - { - $skip = 1; - $nquery++; - $start = 1; - $append = 0; - } - else - { - $skip = 0; + + // Start a new query if necessary + if (!$isAppending) { + if (!empty($currentQuery)) { + // Store the previous query and reset for the next one + $queries[] = ['query' => rtrim($currentQuery, self::INLINE_TRIM), 'delimiter' => $delimiter]; } + $currentQuery = ''; + $isAppending = true; } - if($skip == 0) - { - if($start == 1) - { - $nquery++; - $queryArray[$nquery] = ""; - $delimiterArray[$nquery] = $delimiter; - $start = 0; - } - $queryArray[$nquery] .= $text."\r\n"; - $delimiterArray[$nquery] = $delimiter; - $text = ltrim($text, " \t "); - $start = strlen($text)-strlen($delimiter)-1; - if(stripos(substr($text, $start), $delimiter) !== false || $text == $delimiter) - { - $nquery++; - $start = 1; - $append = 0; - } - else - { - $start = 0; - $append = 1; - } - $delimiterArray[$nquery] = $delimiter; - if(stripos($text, "delimiter ") !== false) - { - $text = trim(preg_replace("/\s+/"," ",$text)); - $arr2 = explode(" ", $text); - $delimiter = $arr2[1]; - $nquery++; - $delimiterArray[$nquery] = $delimiter; - $start = 1; - $append = 0; - } + + // Append current line to the current query + $currentQuery .= $line . "\r\n"; + + // Check if the query ends with the delimiter + if (substr(rtrim($line), -strlen($delimiter)) === $delimiter) { + $isAppending = false; // End of query, so we stop appending } } - $result = array(); - foreach($queryArray as $line=>$sql) - { - $delimiter = $delimiterArray[$line]; - if(stripos($sql, "delimiter ") !== 0) - { - $sql = rtrim($sql, self::INLINE_TRIM); - $sql = substr($sql, 0, strlen($sql)-strlen($delimiter)); - $result[] = array("query"=> $sql, "delimiter"=>$delimiter); - } + + // Add the last query if any + if (!empty($currentQuery)) { + $queries[] = ['query' => rtrim($currentQuery, self::INLINE_TRIM), 'delimiter' => $delimiter]; } - return $result; + + return $queries; } /** diff --git a/src/Util/Database/PicoDatabaseUtilBase.php b/src/Util/Database/PicoDatabaseUtilBase.php index 18eea3c1..51eaff98 100644 --- a/src/Util/Database/PicoDatabaseUtilBase.php +++ b/src/Util/Database/PicoDatabaseUtilBase.php @@ -22,7 +22,7 @@ class PicoDatabaseUtilBase public function getAutoIncrementKey($tableInfo) { $autoIncrement = $tableInfo->getAutoIncrementKeys(); - $autoIncrementKeys = array(); + $autoIncrementKeys = []; if(is_array($autoIncrement) && !empty($autoIncrement)) { foreach($autoIncrement as $col) @@ -61,7 +61,7 @@ public function dumpData($columns, $picoTableName, $data, $maxRecord = 100, $cal // Handle case where fetching data is not required if($data->getFindOption() & MagicObject::FIND_OPTION_NO_FETCH_DATA && $maxRecord > 0 && isset($callbackFunction) && is_callable($callbackFunction)) { - $records = array(); + $records = []; $stmt = $data->getPDOStatement(); // Fetch records in batches while($data = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT)) @@ -81,7 +81,7 @@ public function dumpData($columns, $picoTableName, $data, $maxRecord = 100, $cal call_user_func($callbackFunction, $sql); } // Reset the records buffer - $records = array(); + $records = []; } } // Handle any remaining records @@ -211,7 +211,7 @@ public function createMapTemplate($databaseSource, $databaseTarget, $target) { $targetColumns = array_keys($this->showColumns($databaseTarget, $target)); $sourceColumns = array_keys($this->showColumns($databaseSource, $target)); - $map = array(); + $map = []; foreach($targetColumns as $column) { if(!in_array($column, $sourceColumns)) @@ -306,7 +306,7 @@ public function importDataTable($databaseSource, $databaseTarget, $tableNameSour ->select("*") ->from($sourceTable); $stmt = $databaseSource->query($queryBuilderSource); - $records = array(); + $records = []; while($data = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT)) { $data = $this->processDataMapping($data, $columns, $tableInfo->getMap()); @@ -322,7 +322,7 @@ public function importDataTable($databaseSource, $databaseTarget, $tableNameSour call_user_func($callbackFunction, $sql, $tableNameSource, $tableNameTarget); } // reset buffer - $records = array(); + $records = []; } } if(!empty($records) && isset($callbackFunction) && is_callable($callbackFunction)) @@ -542,7 +542,7 @@ public function fixFloatData($data, $name, $value) public function insert($tableName, $data) { // Collect all unique columns from the data records - $columns = array(); + $columns = []; foreach ($data as $record) { $columns = array_merge($columns, array_keys($record)); } @@ -557,7 +557,7 @@ public function insert($tableName, $data) implode(",\r\n", array_fill(0, count($data), $placeholders)); // Prepare values for binding - $values = array(); + $values = []; foreach ($data as $record) { foreach ($columns as $column) { // Use null if the value is not set diff --git a/src/Util/Database/PicoDatabaseUtilInterface.php b/src/Util/Database/PicoDatabaseUtilInterface.php index ea229551..9f5ac03a 100644 --- a/src/Util/Database/PicoDatabaseUtilInterface.php +++ b/src/Util/Database/PicoDatabaseUtilInterface.php @@ -20,7 +20,7 @@ * Implementations of this interface should consider validation and error handling to ensure data integrity * and security during database operations. */ -interface PicoDatabaseUtilInterface +interface PicoDatabaseUtilInterface //NOSONAR { public function getColumnList($database, $picoTableName); public function getAutoIncrementKey($tableInfo); diff --git a/src/Util/Database/PicoDatabaseUtilMySql.php b/src/Util/Database/PicoDatabaseUtilMySql.php index 82cc71dc..282a487c 100644 --- a/src/Util/Database/PicoDatabaseUtilMySql.php +++ b/src/Util/Database/PicoDatabaseUtilMySql.php @@ -76,8 +76,8 @@ public function getColumnList($database, $tableName) */ public function dumpStructure($tableInfo, $tableName, $createIfNotExists = false, $dropIfExists = false, $engine = 'InnoDB', $charset = 'utf8mb4') { - $query = array(); - $columns = array(); + $query = []; + $columns = []; if($dropIfExists) { $query[] = "-- DROP TABLE IF EXISTS `$tableName`;"; @@ -144,7 +144,7 @@ public function dumpStructure($tableInfo, $tableName, $createIfNotExists = false */ public function createColumn($column) { - $col = array(); + $col = []; $col[] = "\t"; $col[] = "`".$column[parent::KEY_NAME]."`"; $col[] = $column['type']; @@ -208,7 +208,7 @@ public function fixDefaultValue($defaultValue, $type) public function dumpRecord($columns, $tableName, $record) { $value = $record->valueArray(); - $rec = array(); + $rec = []; foreach($value as $key=>$val) { if(isset($columns[$key])) @@ -242,7 +242,7 @@ public function showColumns($database, $tableName) $sql = "SHOW COLUMNS FROM $tableName"; $result = $database->fetchAll($sql, PDO::FETCH_ASSOC); - $columns = array(); + $columns = []; foreach($result as $row) { $columns[$row['Field']] = $row['Type']; @@ -273,7 +273,7 @@ public function autoConfigureImportData($config) $databaseTarget->connect(); $tables = $config->getTable(); - $existingTables = array(); + $existingTables = []; foreach($tables as $tb) { $existingTables[] = $tb->getTarget(); diff --git a/src/Util/Database/PicoDatabaseUtilPostgreSql.php b/src/Util/Database/PicoDatabaseUtilPostgreSql.php index 96208583..cceb85d9 100644 --- a/src/Util/Database/PicoDatabaseUtilPostgreSql.php +++ b/src/Util/Database/PicoDatabaseUtilPostgreSql.php @@ -317,7 +317,7 @@ public function dumpRecord($columns, $tableName, $record) } } - $queryBuilder = new PicoDatabaseQueryBuilder(PicoDatabaseType::DATABASE_TYPE_POSTGRESQL); + $queryBuilder = new PicoDatabaseQueryBuilder(PicoDatabaseType::DATABASE_TYPE_PGSQL); $queryBuilder->newQuery() ->insert() ->into($tableName) diff --git a/src/Util/Database/PicoDatabaseUtilSqlite.php b/src/Util/Database/PicoDatabaseUtilSqlite.php index 508e61f0..616f7c2b 100644 --- a/src/Util/Database/PicoDatabaseUtilSqlite.php +++ b/src/Util/Database/PicoDatabaseUtilSqlite.php @@ -170,7 +170,7 @@ private function getPkeyArrayFinal($pKeyArr, $pKeyArrUsed) * @param array $pKeyArrUsed The array to store used primary key names. * @return array An array containing the determined SQL data type and the updated primary key array. */ - private function determineSqlType($column, $autoIncrementKeys = null, $length = 255, $pKeyArrUsed = array()) + private function determineSqlType($column, $autoIncrementKeys = null, $length = 255, $pKeyArrUsed = []) { $columnName = $column[parent::KEY_NAME]; $columnType = strtolower($column['type']); // Assuming 'type' holds the column type @@ -200,7 +200,8 @@ private function determineSqlType($column, $autoIncrementKeys = null, $length = $sqlType = strtoupper($columnType); if ($sqlType !== 'TINYINT(1)' && $sqlType !== 'FLOAT' && $sqlType !== 'TEXT' && $sqlType !== 'LONGTEXT' && $sqlType !== 'DATE' && $sqlType !== 'TIMESTAMP' && - $sqlType !== 'BLOB') { + $sqlType !== 'BLOB') + { $sqlType = 'VARCHAR(255)'; // Fallback type for unknown types } } @@ -234,7 +235,7 @@ public function getColumnList($database, $tableName) $stmt = $database->query("PRAGMA table_info($tableName)"); // Fetch and display the column details - $rows = array(); + $rows = []; while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $rows[] = array( "Field" => $row['name'], @@ -265,8 +266,8 @@ public function getColumnList($database, $tableName) */ public function dumpStructure($tableInfo, $tableName, $createIfNotExists = false, $dropIfExists = false, $engine = 'InnoDB', $charset = 'utf8mb4') { - $query = array(); - $columns = array(); + $query = []; + $columns = []; if($dropIfExists) { $query[] = "-- DROP TABLE IF EXISTS $tableName;"; @@ -289,7 +290,7 @@ public function dumpStructure($tableInfo, $tableName, $createIfNotExists = false $columns[] = $this->createColumn($column); } $query[] = implode(",\r\n", $columns); - $query[] = ") ENGINE=$engine DEFAULT CHARSET=$charset;"; + $query[] = ") "; $pk = $tableInfo->getPrimaryKeys(); if(isset($pk) && is_array($pk) && !empty($pk)) @@ -333,7 +334,7 @@ public function dumpStructure($tableInfo, $tableName, $createIfNotExists = false */ public function createColumn($column) { - $col = array(); + $col = []; $col[] = "\t"; $col[] = "".$column[parent::KEY_NAME].""; $col[] = $column['type']; @@ -369,11 +370,20 @@ public function createColumn($column) */ public function fixDefaultValue($defaultValue, $type) { - if(strtolower($defaultValue) == 'true' || strtolower($defaultValue) == 'false' || strtolower($defaultValue) == 'null') + if(strtolower($defaultValue) == 'true' + || strtolower($defaultValue) == 'false' + || strtolower($defaultValue) == 'null' + ) { return $defaultValue; } - if(stripos($type, 'enum') !== false || stripos($type, 'char') !== false || stripos($type, 'text') !== false || stripos($type, 'int') !== false || stripos($type, 'float') !== false || stripos($type, 'double') !== false) + if(stripos($type, 'enum') !== false + || stripos($type, 'char') !== false + || stripos($type, 'text') !== false + || stripos($type, 'int') !== false + || stripos($type, 'float') !== false + || stripos($type, 'double') !== false + ) { return "'".$defaultValue."'"; } @@ -401,17 +411,25 @@ public function fixImportData($data, $columns) { $type = $columns[$name]; - if(strtolower($type) == 'tinyint(1)' || strtolower($type) == 'boolean' || strtolower($type) == 'bool') + if(strtolower($type) == 'tinyint(1)' + || strtolower($type) == 'boolean' + || strtolower($type) == 'bool' + ) { // Process boolean types $data = $this->fixBooleanData($data, $name, $value); } - else if(stripos($type, 'integer') !== false || stripos($type, 'int(') !== false) + else if(stripos($type, 'integer') !== false + || stripos($type, 'int(') !== false + ) { // Process integer types $data = $this->fixIntegerData($data, $name, $value); } - else if(stripos($type, 'float') !== false || stripos($type, 'double') !== false || stripos($type, 'decimal') !== false) + else if(stripos($type, 'float') !== false + || stripos($type, 'double') !== false + || stripos($type, 'decimal') !== false + ) { // Process float types $data = $this->fixFloatData($data, $name, $value); @@ -444,7 +462,7 @@ public function autoConfigureImportData($config) $databaseTarget->connect(); $tables = $config->getTable(); - $existingTables = array(); + $existingTables = []; foreach($tables as $tb) { $existingTables[] = $tb->getTarget(); diff --git a/src/Util/Database/PicoSqlParser.php b/src/Util/Database/PicoSqlParser.php index e725b65f..c1675ac5 100644 --- a/src/Util/Database/PicoSqlParser.php +++ b/src/Util/Database/PicoSqlParser.php @@ -26,12 +26,12 @@ class PicoSqlParser { // Constant definitions for keys in the parsed table information - const KEY_COLUMN_NAME = 'Column Name'; - const KEY_PRIMARY_KEY = 'Primary Key'; - const KEY_TYPE = 'Type'; - const KEY_LENGTH = 'Length'; - const KEY_NULLABLE = 'Nullable'; - const KEY_DEFAULT = 'Default'; + const KEY_COLUMN_NAME = 'Field'; + const KEY_PRIMARY_KEY = 'Key'; + const KEY_TYPE = 'Type'; + const KEY_LENGTH = 'Length'; + const KEY_NULLABLE = 'Nullable'; + const KEY_DEFAULT = 'Default'; /** * List of valid SQL data types supported by this parser. @@ -96,7 +96,7 @@ public function parseTable($sql) // NOSONAR preg_match($rg_tb, $sql, $result); $tableName = $result['tb']; - $fld_list = []; + $fldList = []; $primaryKey = null; $columnList = []; @@ -132,7 +132,7 @@ public function parseTable($sql) // NOSONAR { $def = null; } - $fld_list[] = [ + $fldList[] = [ self::KEY_COLUMN_NAME => $columnName, self::KEY_TYPE => trim($rg_fld2_result['ftype']), self::KEY_LENGTH => $length, @@ -148,7 +148,7 @@ public function parseTable($sql) // NOSONAR } if ($primaryKey !== null) { - foreach ($fld_list as &$column) //NOSONAR + foreach ($fldList as &$column) //NOSONAR { if ($column[self::KEY_COLUMN_NAME] === $primaryKey) { $column[self::KEY_PRIMARY_KEY] = true; @@ -160,7 +160,7 @@ public function parseTable($sql) // NOSONAR $x = preg_replace('/(PRIMARY|UNIQUE) KEY\s+[a-zA-Z_0-9\s]+/', '', $f); $x = str_replace(['(', ')'], '', $x); $pkeys = array_map('trim', explode(',', $x)); - foreach ($fld_list as &$column) { + foreach ($fldList as &$column) { if ($this->inArray($pkeys, $column[self::KEY_COLUMN_NAME])) { $column[self::KEY_PRIMARY_KEY] = true; } @@ -169,7 +169,7 @@ public function parseTable($sql) // NOSONAR } return [ 'tableName' => $tableName, - 'columns' => $fld_list, + 'columns' => $fldList, 'primaryKey' => $primaryKey ]; } @@ -232,6 +232,7 @@ public function init() */ public function parseAll($sql) { + $sql = str_replace("`", "", $sql); $inf = []; $rg_tb = '/(create\s+table\s+if\s+not\s+exists|create\s+table)\s+(?.*)\s+\(/i'; @@ -265,4 +266,5 @@ public function getTableInfo() { return $this->tableInfo; } + } diff --git a/src/Util/PicoParsedown.php b/src/Util/PicoParsedown.php index 6ac9a213..d10e6f29 100644 --- a/src/Util/PicoParsedown.php +++ b/src/Util/PicoParsedown.php @@ -208,12 +208,12 @@ public function setSafeMode($safeMode) */ protected function lines(array $lines) //NOSONAR { - $CurrentBlock = null; + $currentBlock = null; foreach ($lines as $line) { if (chop($line) === '') { - if (isset($CurrentBlock)) { - $CurrentBlock['interrupted'] = true; + if (isset($currentBlock)) { + $currentBlock['interrupted'] = true; } continue; @@ -248,16 +248,16 @@ protected function lines(array $lines) //NOSONAR # ~ - if (isset($CurrentBlock['continuable'])) { - $block = $this->{'block' . $CurrentBlock['type'] . 'Continue'}($line, $CurrentBlock); + if (isset($currentBlock['continuable'])) { + $block = $this->{'block' . $currentBlock['type'] . 'Continue'}($line, $currentBlock); if (isset($block)) { - $CurrentBlock = $block; + $currentBlock = $block; continue; } else { - if ($this->isBlockCompletable($CurrentBlock['type'])) { - $CurrentBlock = $this->{'block' . $CurrentBlock['type'] . 'Complete'}($CurrentBlock); + if ($this->isBlockCompletable($currentBlock['type'])) { + $currentBlock = $this->{'block' . $currentBlock['type'] . 'Complete'}($currentBlock); } } } @@ -280,13 +280,13 @@ protected function lines(array $lines) //NOSONAR # ~ foreach ($blockTypes as $blockType) { - $block = $this->{'block' . $blockType}($line, $CurrentBlock); + $block = $this->{'block' . $blockType}($line, $currentBlock); if (isset($block)) { $block['type'] = $blockType; if (!isset($block['identified'])) { - $Blocks[] = $CurrentBlock; + $blocks[] = $currentBlock; $block['identified'] = true; } @@ -295,7 +295,7 @@ protected function lines(array $lines) //NOSONAR $block['continuable'] = true; } - $CurrentBlock = $block; + $currentBlock = $block; continue 2; } @@ -303,34 +303,34 @@ protected function lines(array $lines) //NOSONAR # ~ - if (isset($CurrentBlock) && !isset($CurrentBlock['type']) && !isset($CurrentBlock['interrupted'])) { - $CurrentBlock['element']['text'] .= "\n" . $text; + if (isset($currentBlock) && !isset($currentBlock['type']) && !isset($currentBlock['interrupted'])) { + $currentBlock['element']['text'] .= "\n" . $text; } else { - $Blocks[] = $CurrentBlock; + $blocks[] = $currentBlock; - $CurrentBlock = $this->paragraph($line); + $currentBlock = $this->paragraph($line); - $CurrentBlock['identified'] = true; + $currentBlock['identified'] = true; } } # ~ - if (isset($CurrentBlock['continuable']) && $this->isBlockCompletable($CurrentBlock['type'])) { - $CurrentBlock = $this->{'block' . $CurrentBlock['type'] . 'Complete'}($CurrentBlock); + if (isset($currentBlock['continuable']) && $this->isBlockCompletable($currentBlock['type'])) { + $currentBlock = $this->{'block' . $currentBlock['type'] . 'Complete'}($currentBlock); } # ~ - $Blocks[] = $CurrentBlock; + $blocks[] = $currentBlock; - unset($Blocks[0]); + unset($blocks[0]); # ~ $markup = ''; - foreach ($Blocks as $block) { + foreach ($blocks as $block) { if (isset($block['hidden'])) { continue; } @@ -349,12 +349,12 @@ protected function lines(array $lines) //NOSONAR /** * Check if the specified block type can be continued. * - * @param string $Type The type of the block to check. + * @param string $type The type of the block to check. * @return bool True if the block type can be continued, false otherwise. */ - protected function isBlockContinuable($Type) + protected function isBlockContinuable($type) { - return method_exists($this, 'block' . $Type . 'Continue'); + return method_exists($this, 'block' . $type . 'Continue'); } /** @@ -362,12 +362,12 @@ protected function isBlockContinuable($Type) * * This method checks if a method exists for completing a block of the specified type. * - * @param string $Type The block type to check. + * @param string $type The block type to check. * @return bool True if the block type can be completed, false otherwise. */ - protected function isBlockCompletable($Type) + protected function isBlockCompletable($type) { - return method_exists($this, 'block' . $Type . 'Complete'); + return method_exists($this, 'block' . $type . 'Complete'); } /** @@ -965,16 +965,16 @@ protected function blockReference($line) { $id = strtolower($matches[1]); - $Data = array( + $data = array( 'url' => $matches[2], 'title' => null, ); if (isset($matches[3])) { - $Data['title'] = $matches[3]; + $data['title'] = $matches[3]; } - $this->definitionData['Reference'][$id] = $Data; + $this->definitionData['Reference'][$id] = $data; return array( 'hidden' => true, @@ -1030,7 +1030,7 @@ protected function blockTable($line, array $block = null) //NOSONAR # ~ - $HeaderElements = array(); + $headerElements = array(); $header = $block['element']['text']; @@ -1042,7 +1042,7 @@ protected function blockTable($line, array $block = null) //NOSONAR foreach ($headerCells as $index => $headerCell) { $headerCell = trim($headerCell); - $HeaderElement = array( + $headerElement = array( 'name' => 'th', 'text' => $headerCell, 'handler' => 'line', @@ -1051,12 +1051,12 @@ protected function blockTable($line, array $block = null) //NOSONAR if (isset($alignments[$index])) { $alignment = $alignments[$index]; - $HeaderElement['attributes'] = array( + $headerElement['attributes'] = array( 'style' => 'text-align: ' . $alignment . ';', ); } - $HeaderElements[] = $HeaderElement; + $headerElements[] = $headerElement; } # ~ @@ -1084,7 +1084,7 @@ protected function blockTable($line, array $block = null) //NOSONAR $block['element']['text'][0]['text'][] = array( 'name' => 'tr', 'handler' => 'elements', - 'text' => $HeaderElements, + 'text' => $headerElements, ); return $block; @@ -1107,7 +1107,7 @@ protected function blockTableContinue($line, $block) //NOSONAR } if ($line['text'][0] === '|' || strpos($line['text'], '|')) { - $Elements = array(); + $elements = array(); $row = $line['text']; @@ -1131,13 +1131,13 @@ protected function blockTableContinue($line, $block) //NOSONAR ); } - $Elements[] = $element; + $elements[] = $element; } $element = array( 'name' => 'tr', 'handler' => 'elements', - 'text' => $Elements, + 'text' => $elements, ); $block['element']['text'][1]['text'][] = $element; @@ -1214,7 +1214,7 @@ public function line($text, $nonNestables = array()) //NOSONAR $markerPosition = strpos($text, $marker); - $Excerpt = array('text' => $excerpt, 'context' => $text); + $excerpt = array('text' => $excerpt, 'context' => $text); foreach ($this->inlineTypes[$marker] as $inlineType) { # check to see if the current inline type is nestable in the current context @@ -1223,41 +1223,41 @@ public function line($text, $nonNestables = array()) //NOSONAR continue; } - $Inline = $this->{'inline' . $inlineType}($Excerpt); + $inline = $this->{'inline' . $inlineType}($excerpt); - if (!isset($Inline)) { + if (!isset($inline)) { continue; } # makes sure that the inline belongs to "our" marker - if (isset($Inline['position']) && $Inline['position'] > $markerPosition) { + if (isset($inline['position']) && $inline['position'] > $markerPosition) { continue; } # sets a default inline position - if (!isset($Inline['position'])) { - $Inline['position'] = $markerPosition; + if (!isset($inline['position'])) { + $inline['position'] = $markerPosition; } # cause the new element to 'inherit' our non nestables foreach ($nonNestables as $non_nestable) { - $Inline['element']['nonNestables'][] = $non_nestable; + $inline['element']['nonNestables'][] = $non_nestable; } # the text that comes before the inline - $unmarkedText = substr($text, 0, $Inline['position']); + $unmarkedText = substr($text, 0, $inline['position']); # compile the unmarked text $markup .= $this->unmarkedText($unmarkedText); # compile the inline - $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + $markup .= isset($inline['markup']) ? $inline['markup'] : $this->element($inline['element']); # remove the examined text - $text = substr($text, $Inline['position'] + $Inline['extent']); + $text = substr($text, $inline['position'] + $inline['extent']); continue 2; } @@ -1281,14 +1281,14 @@ public function line($text, $nonNestables = array()) //NOSONAR * * This method checks for code markers and captures the code content. * - * @param array $Excerpt The excerpt to process. + * @param array $excerpt The excerpt to process. * @return array|null The inline code element or null if not applicable. */ - protected function inlineCode($Excerpt) + protected function inlineCode($excerpt) { - $marker = $Excerpt['text'][0]; + $marker = $excerpt['text'][0]; - if (preg_match('/^(' . $marker . '+)[ ]*(.+?)[ ]*(?') !== false && preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) { + if (strpos($excerpt['text'], '>') !== false && preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $excerpt['text'], $matches)) { $url = $matches[1]; if (!isset($matches[2])) { @@ -1337,20 +1337,20 @@ protected function inlineEmailTag($Excerpt) * * This method checks for markers indicating strong or emphasized text. * - * @param array $Excerpt The excerpt to process. + * @param array $excerpt The excerpt to process. * @return array|null The emphasis element or null if not applicable. */ - protected function inlineEmphasis($Excerpt) + protected function inlineEmphasis($excerpt) { - if (!isset($Excerpt['text'][1])) { + if (!isset($excerpt['text'][1])) { return null; } - $marker = $Excerpt['text'][0]; + $marker = $excerpt['text'][0]; - if ($Excerpt['text'][1] === $marker && preg_match($this->strongRegex[$marker], $Excerpt['text'], $matches)) { + if ($excerpt['text'][1] === $marker && preg_match($this->strongRegex[$marker], $excerpt['text'], $matches)) { $emphasis = 'strong'; - } elseif (preg_match($this->emRegex[$marker], $Excerpt['text'], $matches)) { + } elseif (preg_match($this->emRegex[$marker], $excerpt['text'], $matches)) { $emphasis = 'em'; } else { return null; @@ -1371,14 +1371,14 @@ protected function inlineEmphasis($Excerpt) * * This method checks for special characters that are preceded by a backslash. * - * @param array $Excerpt The excerpt to process. + * @param array $excerpt The excerpt to process. * @return array|null The escaped character element or null if not applicable. */ - protected function inlineEscapeSequence($Excerpt) + protected function inlineEscapeSequence($excerpt) { - if (isset($Excerpt['text'][1]) && in_array($Excerpt['text'][1], $this->specialCharacters)) { + if (isset($excerpt['text'][1]) && in_array($excerpt['text'][1], $this->specialCharacters)) { return array( - 'markup' => $Excerpt['text'][1], + 'markup' => $excerpt['text'][1], 'extent' => 2, ); } @@ -1389,48 +1389,48 @@ protected function inlineEscapeSequence($Excerpt) * * This method identifies image links formatted with brackets and captures the source and alt text. * - * @param array $Excerpt The excerpt to process. + * @param array $excerpt The excerpt to process. * @return array|null The image element or null if not applicable. */ - protected function inlineImage($Excerpt) + protected function inlineImage($excerpt) { - if (!isset($Excerpt['text'][1]) || $Excerpt['text'][1] !== '[') { + if (!isset($excerpt['text'][1]) || $excerpt['text'][1] !== '[') { return null; } - $Excerpt['text'] = substr($Excerpt['text'], 1); + $excerpt['text'] = substr($excerpt['text'], 1); - $Link = $this->inlineLink($Excerpt); + $link = $this->inlineLink($excerpt); - if ($Link === null) { + if ($link === null) { return null; } - $Inline = array( - 'extent' => $Link['extent'] + 1, + $inline = array( + 'extent' => $link['extent'] + 1, 'element' => array( 'name' => 'img', 'attributes' => array( - 'src' => $Link['element']['attributes']['href'], - 'alt' => $Link['element']['text'], + 'src' => $link['element']['attributes']['href'], + 'alt' => $link['element']['text'], ), ), ); - $Inline['element']['attributes'] += $Link['element']['attributes']; + $inline['element']['attributes'] += $link['element']['attributes']; - unset($Inline['element']['attributes']['href']); + unset($inline['element']['attributes']['href']); - return $Inline; + return $inline; } /** * Processes inline links in the provided text. * - * @param array $Excerpt Contains the text to be processed. + * @param array $excerpt Contains the text to be processed. * @return array|null Returns an array containing the extent of the match and the link element, or null if no match is found. */ - protected function inlineLink($Excerpt) + protected function inlineLink($excerpt) { $element = array( 'name' => 'a', @@ -1445,7 +1445,7 @@ protected function inlineLink($Excerpt) $extent = 0; - $remainder = $Excerpt['text']; + $remainder = $excerpt['text']; if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) //NOSONAR { @@ -1481,10 +1481,10 @@ protected function inlineLink($Excerpt) return null; } - $Definition = $this->definitionData['Reference'][$definition]; + $definition = $this->definitionData['Reference'][$definition]; - $element['attributes']['href'] = $Definition['url']; - $element['attributes']['title'] = $Definition['title']; + $element['attributes']['href'] = $definition['url']; + $element['attributes']['title'] = $definition['title']; } return array( @@ -1496,16 +1496,16 @@ protected function inlineLink($Excerpt) /** * Processes inline HTML markup in the provided text. * - * @param array $Excerpt Contains the text to be processed. + * @param array $excerpt Contains the text to be processed. * @return array|null Returns an array with the markup and its extent, or null if no valid markup is found. */ - protected function inlineMarkup($Excerpt) //NOSONAR + protected function inlineMarkup($excerpt) //NOSONAR { - if ($this->markupEscaped || $this->safeMode || strpos($Excerpt['text'], '>') === false) { + if ($this->markupEscaped || $this->safeMode || strpos($excerpt['text'], '>') === false) { return null; } - if ($Excerpt['text'][1] === '/' && preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) //NOSONAR + if ($excerpt['text'][1] === '/' && preg_match('/^<\/\w[\w-]*[ ]*>/s', $excerpt['text'], $matches)) //NOSONAR { return array( 'markup' => $matches[0], @@ -1513,7 +1513,7 @@ protected function inlineMarkup($Excerpt) //NOSONAR ); } - if ($Excerpt['text'][1] === '!' && preg_match('/^/s', $Excerpt['text'], $matches)) //NOSONAR + if ($excerpt['text'][1] === '!' && preg_match('/^/s', $excerpt['text'], $matches)) //NOSONAR { return array( 'markup' => $matches[0], @@ -1521,7 +1521,7 @@ protected function inlineMarkup($Excerpt) //NOSONAR ); } - if ($Excerpt['text'][1] !== ' ' && preg_match('/^<\w[\w-]*(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*\/?>/s', $Excerpt['text'], $matches)) { + if ($excerpt['text'][1] !== ' ' && preg_match('/^<\w[\w-]*(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*\/?>/s', $excerpt['text'], $matches)) { return array( 'markup' => $matches[0], 'extent' => strlen($matches[0]), @@ -1532,23 +1532,23 @@ protected function inlineMarkup($Excerpt) //NOSONAR /** * Processes inline special characters in the provided text. * - * @param array $Excerpt Contains the text to be processed. + * @param array $excerpt Contains the text to be processed. * @return array|null Returns an array with the escaped character and its extent, or null if no special character is found. */ - protected function inlineSpecialCharacter($Excerpt) + protected function inlineSpecialCharacter($excerpt) { - if ($Excerpt['text'][0] === '&' && !preg_match('/^&#?\w+;/', $Excerpt['text'])) { + if ($excerpt['text'][0] === '&' && !preg_match('/^&#?\w+;/', $excerpt['text'])) { return array( 'markup' => '&', 'extent' => 1, ); } - $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + $specialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); - if (isset($SpecialCharacter[$Excerpt['text'][0]])) { + if (isset($specialCharacter[$excerpt['text'][0]])) { return array( - 'markup' => '&' . $SpecialCharacter[$Excerpt['text'][0]] . ';', + 'markup' => '&' . $specialCharacter[$excerpt['text'][0]] . ';', 'extent' => 1, ); } @@ -1557,16 +1557,16 @@ protected function inlineSpecialCharacter($Excerpt) /** * Processes inline strikethrough text in the provided text. * - * @param array $Excerpt Contains the text to be processed. + * @param array $excerpt Contains the text to be processed. * @return array|null Returns an array with the extent of the strikethrough and the corresponding element, or null if no match is found. */ - protected function inlineStrikethrough($Excerpt) + protected function inlineStrikethrough($excerpt) { - if (!isset($Excerpt['text'][1])) { + if (!isset($excerpt['text'][1])) { return null; } - if ($Excerpt['text'][1] === '~' && preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) { + if ($excerpt['text'][1] === '~' && preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $excerpt['text'], $matches)) { return array( 'extent' => strlen($matches[0]), 'element' => array( @@ -1581,16 +1581,16 @@ protected function inlineStrikethrough($Excerpt) /** * Processes inline URLs in the provided text. * - * @param array $Excerpt Contains the text to be processed. + * @param array $excerpt Contains the text to be processed. * @return array|null Returns an array with the extent and URL element, or null if no valid URL is found. */ - protected function inlineUrl($Excerpt) + protected function inlineUrl($excerpt) { - if ($this->urlsLinked !== true || !isset($Excerpt['text'][2]) || $Excerpt['text'][2] !== '/') { + if ($this->urlsLinked !== true || !isset($excerpt['text'][2]) || $excerpt['text'][2] !== '/') { return null; } - if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) //NOSONAR + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) //NOSONAR { $url = $matches[0][0]; @@ -1612,12 +1612,12 @@ protected function inlineUrl($Excerpt) /** * Processes inline URL tags formatted in angle brackets. * - * @param array $Excerpt Contains the text to be processed. + * @param array $excerpt Contains the text to be processed. * @return array|null Returns an array with the extent and URL element, or null if no valid tag is found. */ - protected function inlineUrlTag($Excerpt) + protected function inlineUrlTag($excerpt) { - if (strpos($Excerpt['text'], '>') !== false && preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) { + if (strpos($excerpt['text'], '>') !== false && preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $excerpt['text'], $matches)) { $url = $matches[1]; return array( @@ -1714,14 +1714,14 @@ protected function element(array $element) //NOSONAR /** * Generates HTML markup for multiple elements. * - * @param array $Elements An array of elements to be rendered as HTML. + * @param array $elements An array of elements to be rendered as HTML. * @return string The generated HTML markup for all elements. */ - protected function elements(array $Elements) + protected function elements(array $elements) { $markup = ''; - foreach ($Elements as $element) { + foreach ($elements as $element) { $markup .= "\n" . $this->element($element); } diff --git a/tests/caller.php b/tests/caller.php index ce52c949..966e84d8 100644 --- a/tests/caller.php +++ b/tests/caller.php @@ -1,6 +1,10 @@ executeNativeQuery(); @@ -271,7 +275,7 @@ public function native13($timeCreate, $aktif) $obj = new Supervisor(null, $database); -/* + $native1 = $obj->native1(1, true); $native2 = $obj->native2(1, true); @@ -317,17 +321,24 @@ public function native13($timeCreate, $aktif) // For the MagicObject return type, users can utilize the features of the MagicObject except for interacting with the database again because native queries are designed for a different purpose. -echo "Alamat: " . $native8->getTelepon() . "\r\n"; -echo "Alamat: " . $native9[0]->getTelepon() . "\r\n"; -echo "Alamat: " . $native10->getTelepon() . "\r\n"; -echo "Alamat: " . $native11[0]->getTelepon() . "\r\n"; -*/ +echo "Telepon: " . $native8->getTelepon() . "\r\n"; +echo "Telepon: " . $native9[0]->getTelepon() . "\r\n"; +echo "Telepon: " . $native10->getTelepon() . "\r\n"; +echo "Telepon: " . $native11[0]->getTelepon() . "\r\n"; + + +$sortable = new PicoSortable(); +$sortable->addSortable(new PicoSort("nama", PicoSort::ORDER_TYPE_ASC)); +$pageable = new PicoPageable(new PicoPage(1, 2)); try { -$native13 = $obj->native13([new DateTime('2025-03-27 15:23:47', new DateTimeZone($databaseCredential->getDatabase()->getTimeZone()))], true); -echo "\r\nnative13:\r\n"; -echo ($native13); + $native13 = $obj->native13($pageable, $sortable, true); + echo "\r\nnative13:\r\n"; + foreach($native13 as $sup) + { + echo $sup."\r\n\r\n"; + } } catch(Exception $e) { diff --git a/tests/configure-import.php b/tests/configure-import.php index 8643631e..207c62ab 100644 --- a/tests/configure-import.php +++ b/tests/configure-import.php @@ -8,6 +8,6 @@ $config = new SecretObject(); $config->loadYamlFile('import.yml', true, true, true); -PicoDatabaseUtilMySql::autoConfigureImportData($config); +(new PicoDatabaseUtilMySql())->autoConfigureImportData($config); file_put_contents('import.yml', $config->dumpYaml(0, 2)); diff --git a/tests/dto.php b/tests/dto.php index 9f7206dc..292d32be 100644 --- a/tests/dto.php +++ b/tests/dto.php @@ -694,7 +694,6 @@ public function onLoadData($data) $data->setName("Malik"); $data->getProducer()->setName("aaaaa"); } - throw new InvalidParameterException("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); return $data; } } diff --git a/tests/entity.php b/tests/entity.php index 79b2d7eb..e8bd30c7 100644 --- a/tests/entity.php +++ b/tests/entity.php @@ -629,6 +629,7 @@ class Artist extends MagicObject $database->connect(); $album = new EntityAlbum(null, $database); + try { /* diff --git a/tests/titip.php b/tests/titip.php new file mode 100644 index 00000000..f968594b --- /dev/null +++ b/tests/titip.php @@ -0,0 +1,227 @@ +/** + * Executes a database query based on the parameters and annotations from the caller function. + * + * This method uses reflection to retrieve the query string from the caller's docblock, + * bind the parameters, and execute the query against the database. + * + * It analyzes the parameters and return type of the caller function, enabling dynamic query + * execution tailored to the specified return type. Supported return types include: + * - `void`: Returns null. + * - `int` or `integer`: Returns the number of affected rows. + * - `object` or `stdClass`: Returns a single result as an object. + * - `stdClass[]`: Returns all results as an array of stdClass objects. + * - `array`: Returns all results as an associative array. + * - `string`: Returns the JSON-encoded results. + * - `PDOStatement`: Returns the prepared statement for further operations if needed. + * - `MagicObject` and its derived classes: If the return type is a class name or an array of class names, + * instances of that class will be created for each row fetched. + * + * @return mixed Returns the result based on the return type of the caller function: + * - null if the return type is void. + * - integer for the number of affected rows if the return type is int. + * - object for a single result if the return type is object. + * - an array of associative arrays for multiple results if the return type is array. + * - a JSON string if the return type is string. + * - instances of a specified class if the return type matches a class name. + * + * @throws PDOException If there is an error executing the database query. + * @throws InvalidQueryInputException If there is no query to be executed. + * @throws InvalidReturnTypeException If the return type specified is invalid. + */ + protected function executeNativeQuery() //NOSONAR + { + // Retrieve caller trace information + $trace = debug_backtrace(); + + // Get parameters from the caller function + $callerParamValues = isset($trace[1]['args']) ? $trace[1]['args'] : []; + + // Get the name of the caller function and class + $callerFunctionName = $trace[1]['function']; + $callerClassName = $trace[1]['class']; + + // Use reflection to get annotations from the caller function + $reflection = new ReflectionMethod($callerClassName, $callerFunctionName); + $docComment = $reflection->getDocComment(); + + // Get the query from the @query annotation + preg_match('/@query\s*\("([^"]+)"\)/', $docComment, $matches); + $queryString = $matches ? $matches[1] : ''; + + $queryString = trim($queryString, " \r\n\t "); + if(empty($queryString)) + { + // Try reading the query in another way + preg_match('/@query\s*\(\s*"(.*?)"\s*\)/s', $docComment, $matches); + $queryString = $matches ? $matches[1] : ''; + if(empty($queryString)) + { + throw new InvalidQueryInputException("No query found.\r\n".$docComment); + } + } + + // Get parameter information from the caller function + $callerParams = $reflection->getParameters(); + + // Get return type from the caller function + preg_match('/@return\s+([^\s]+)/', $docComment, $matches); + $returnType = $matches ? $matches[1] : 'void'; + + // Trim return type + $returnType = trim($returnType); + + // Change self to callerClassName + if($returnType == "self[]") + { + $returnType = $callerClassName."[]"; + } + else if($returnType == "self") + { + $returnType = $callerClassName; + } + + $params = []; + $pageable = null; + $sortable = null; + try { + // Get database connection + $pdo = $this->_database->getDatabaseConnection(); + + // Replace array + foreach ($callerParamValues as $index => $paramValue) { + if($paramValue instanceof PicoPageable) + { + $pageable = $paramValue; + } + else if($paramValue instanceof PicoSortable) + { + $sortable = $paramValue; + } + else if (isset($callerParams[$index])) { + // Format parameter name according to the query + $paramName = $callerParams[$index]->getName(); + if(is_array($paramValue)) + { + $queryString = str_replace(":".$paramName, PicoDatabaseUtil::toList($paramValue, true, true), $queryString); + } + } + } + + if(isset($pageable) || isset($sortable)) + { + $queryBuilder = new PicoDatabaseQueryBuilder($this->_database->getDatabaseType()); + $queryString = $queryBuilder->addPaginationAndSorting($queryString, $pageable, $sortable); + } + + $stmt = $pdo->prepare($queryString); + + // Automatically bind each parameter + foreach ($callerParamValues as $index => $paramValue) { + if (isset($callerParams[$index])) { + if($paramValue instanceof PicoPageable || $paramValue instanceof PicoSortable) + { + // skip + } + else + { + // Format parameter name according to the query + $paramName = $callerParams[$index]->getName(); + if(!is_array($paramValue)) + { + $maped = $this->mapToPdoParamType($paramValue); + $paramType = $maped->type; + $paramValue = $maped->value; + $params[$paramName] = $paramValue; + $stmt->bindValue(":".$paramName, $paramValue, $paramType); + } + } + } + } + + // Send query to logger + $debugFunction = $this->_database->getCallbackDebugQuery(); + if(isset($debugFunction) && is_callable($debugFunction)) + { + call_user_func($debugFunction, PicoDatabaseUtil::getFinalQuery($stmt, $params)); + } + + // Execute the query + $stmt->execute(); + + if ($returnType == "void") { + // Return null if the return type is void + return null; + } + if ($returnType == "PDOStatement") { + // Return the PDOStatement object + return $stmt; + } else if ($returnType == "int" || $returnType == "integer") { + // Return the affected row count + return $stmt->rowCount(); + } else if ($returnType == "object" || $returnType == "stdClass") { + // Return one row as an object + return $stmt->fetch(PDO::FETCH_OBJ); + } else if ($returnType == "array") { + // Return all rows as an associative array + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } else if ($returnType == "string") { + // Return the result as a JSON string + return json_encode($stmt->fetchAll(PDO::FETCH_OBJ)); + } else { + try { + // Check for array-type hinting in the return type + if (stripos($returnType, "[") !== false) { + $className = trim(explode("[", $returnType)[0]); + if ($className == "stdClass") { + // Return all rows as stdClass objects + return $stmt->fetchAll(PDO::FETCH_OBJ); + } + else if($className == 'MagicObject') { + $result = $stmt->fetchAll(PDO::FETCH_OBJ); + $ret = []; + foreach ($result as $row) { + $ret[] = new MagicObject($row); + } + return $ret; + } + else if (class_exists($className)) { + // Map result rows to the specified class + $obj = new $className(); + if($obj instanceof MagicObject) { + $result = $stmt->fetchAll(PDO::FETCH_OBJ); + foreach ($result as $row) { + $ret[] = new $className($row); + } + return $ret; + } + } + throw new InvalidReturnTypeException("Invalid return type for $className"); + } else { + // Return a single object of the specified class + $className = trim($returnType); + if($className == 'MagicObject') { + $row = $stmt->fetch(PDO::FETCH_OBJ); + return new MagicObject($row); + } + else if (class_exists($className)) { + $obj = new $className(); + if($obj instanceof MagicObject) { + $row = $stmt->fetch(PDO::FETCH_OBJ); + return $obj->loadData($row); + } + } + throw new InvalidReturnTypeException("Invalid return type for $className"); + } + } catch (Exception $e) { + // Log the exception if the class is not found + throw new InvalidReturnTypeException("Invalid return type for $className"); + } + } + } + catch (PDOException $e) + { + // Handle database errors with logging + throw new PDOException($e->getMessage(), $e->getCode(), $e); + } + return null; + } \ No newline at end of file diff --git a/tutorial.md b/tutorial.md index bcbe5e3e..08ce1cd2 100644 --- a/tutorial.md +++ b/tutorial.md @@ -2421,6 +2421,28 @@ This implementation provides a robust framework for session management in a PHP - **Callbacks**: Support for custom callback functions for query execution and debugging. - **Unique ID Generation**: Generate unique identifiers for database records. +### Database Support + +MagicObject supports the following databases: + +1. **MySQL** + + One of the most popular open-source relational databases, known for its speed, reliability, and ease of use. MySQL is widely used in web applications and offers strong performance, security features, and support for SQL standards. + +2. **MariaDB** + + A fork of MySQL, created by the original developers of MySQL after concerns over Oracle’s acquisition of MySQL. MariaDB is designed to maintain compatibility with MySQL while adding new features and optimizations. It is fully open-source and highly regarded for its performance and stability. + +3. **PostgreSQL** + + A powerful, open-source relational database system known for its robustness, SQL compliance, and extensive feature set, including ACID compliance, JSON support, and advanced indexing mechanisms. + +4. **SQLite** + + A lightweight, serverless, self-contained SQL database engine that is highly portable. It is often used for embedded systems or small-scale applications due to its minimal setup and resource usage. Despite its simplicity, SQLite supports a wide range of SQL features and is widely used in mobile apps and other local storage scenarios. + +MagicObject’s compatibility with these databases enables flexible, scalable, and efficient data management across different platforms and environments. + ### Installation To use the `PicoDatabase` class, ensure you have PHP with PDO support. Include the class file in your project, and you can instantiate it with your database credentials. @@ -2650,7 +2672,7 @@ public function query($sql, $params = null) - `array|null $params`: Optional parameters for the SQL query. **Returns**: PDOStatement object or `false` on failure. -##### Fetch a Single Result +#### Fetch a Single Result ```php public function fetch($sql, $tentativeType = PDO::FETCH_ASSOC, $defaultValue = null, $params = null) @@ -2753,6 +2775,37 @@ try { ### Conclusion `PicoDatabase` is a robust class for managing database operations in PHP applications. By following the examples and method descriptions provided in this manual, you can effectively utilize its features for your database interactions. For further assistance, refer to the source code and documentation available at [MagicObject GitHub](https://github.com/Planetbiru/MagicObject). + +## MagicObject with PDO + +### Overview + +With the release of **MagicObject 2.7**, a significant update has been introduced to allow users to leverage **PDO** (PHP Data Objects) for database connections. In previous versions, **MagicObject** required the use of **PicoDatabase**, its custom database handling class. However, recognizing that many developers are accustomed to establishing database connections via traditional PDO, this new version introduces flexibility by allowing PDO connections to be passed directly to the **MagicObject** constructor. + +This update aims to bridge the gap between traditional PDO-based database management and the advanced features provided by **MagicObject**, thus enhancing compatibility while retaining all the powerful functionality of the framework. + +### Why PDO Support? + +The decision to support **PDO** was made to accommodate users who have already established database connections in their applications using PDO, instead of relying on **PicoDatabase** from the start. By supporting PDO, **MagicObject** allows users to continue working with their preferred method of connecting to the database while still benefiting from the full range of features and utilities **MagicObject** offers. + +While PDO is now an option for initializing **MagicObject**, it is used only in the constructor. Once the object is initialized, **MagicObject** continues to use **PicoDatabase** for all subsequent database interactions, ensuring that users can still benefit from **PicoDatabase**'s advanced features like automatic query building, database abstraction, and optimized query execution. + +### How PDO Support Works + +In **MagicObject 2.7**, when you pass a **PDO** connection object to the constructor, it is automatically converted into a **PicoDatabase** instance using the `PicoDatabase::fromPdo()` static method. This ensures that even though PDO is used to establish the initial connection, the object will still operate using **PicoDatabase** for all subsequent database operations. The constructor of **MagicObject** ensures that the database connection is properly initialized and the type of database is correctly detected based on the PDO driver. + +### Benefits of PDO Support in MagicObject 2.7 + +- **Compatibility**: This change makes **MagicObject** more compatible with existing applications that are already using PDO for database connections. Developers can continue to use PDO for initializing connections while taking advantage of **PicoDatabase**'s advanced database features for the rest of the application. + +- **Flexibility**: Developers now have the flexibility to choose between traditional PDO connections and **PicoDatabase**, depending on their needs. This is especially useful for applications transitioning to **MagicObject** but needing to maintain compatibility with existing database handling code. + +- **Ease of Transition**: By supporting PDO in the constructor, **MagicObject** makes it easier for developers to gradually adopt its features without the need to refactor existing database handling code. + +### Conclusion + +Version 2.7 of **MagicObject** introduces an important enhancement by allowing PDO connections to be used alongside **PicoDatabase**. This update provides greater flexibility for developers, allowing them to work with traditional PDO connections if they choose, while still benefiting from the advanced features of **MagicObject** for database interactions. This change aligns with the goal of making **MagicObject** more accessible to a wider range of developers, whether they are just starting with **MagicObject** or are looking to transition from an existing PDO-based application. + ## Entity Entity is class to access database. Entity is derived from MagicObject. Some annotations required to activated all entity features. @@ -9698,6 +9751,14 @@ Native query must be a function of a class that extends from the MagicObject cla Native queries can be created on entities used by the application. If in the previous version the entity only contained properties, then in version 2.0, the entity can also contain functions for native queries. However, entities in versions 1 and 2 both support functions but functions with native queries are only supported in version 2.0. +### Pagination and Sorting + +In **MagicObject version 2.7**, support for **pageable** and **sortable** functionality has been added to native queries. Previously, native queries did not support pagination and sorting directly. Instead, users had to manually include `SORT BY` and `LIMIT OFFSET` clauses in their queries, which made them less flexible. This approach was problematic because each Database Management System (DBMS) has its own syntax for writing queries, making it cumbersome to adapt queries for different platforms. + +With the introduction of pageable and sortable support in version 2.7, users can now easily pass **pagination** parameters using the `PicoPageable` type and **sorting** parameters using the `PicoSortable` type directly into their native queries. These parameters can be placed anywhere within the query, but it is recommended to position them either at the beginning or the end of the query for optimal readability and organization. + +This enhancement makes native queries more flexible and easier to maintain, as the logic for pagination and sorting is handled automatically, without requiring manual intervention for each DBMS. As a result, users can now write cleaner, more efficient, and database-agnostic native queries. + ### Debug Query MagicObject checks if the database connection has a debugging function for queries. If available, it sends the executed query along with the parameter values to this function, aiding users in identifying errors during query definition and execution. @@ -9973,6 +10034,28 @@ class Supervisor extends MagicObject // Call parent method to execute the query return $this->executeNativeQuery(); } + + /** + * Native query 13 + * + * This method will return a prepared statement for further operations if necessary. + * + * @param PicoPagebale $pageable + * @param PicoSortable $sortable + * @param bool $aktif The active status to filter results. + * @return MagicObject[] + * @query(" + SELECT supervisor.* + FROM supervisor + WHERE supervisor.supervisor_id in :supervisorId + AND supervisor.aktif = :aktif + ") + */ + public function native13($pageable, $sortable, $aktif) + { + // Call parent method to execute the query + return $this->executeNativeQuery(); + } } $obj = new Supervisor(null, $database); @@ -10026,6 +10109,25 @@ echo "Alamat: " . $native8->getTelepon() . "\r\n"; echo "Alamat: " . $native9[0]->getTelepon() . "\r\n"; echo "Alamat: " . $native10->getTelepon() . "\r\n"; echo "Alamat: " . $native11[0]->getTelepon() . "\r\n"; + + +$sortable = new PicoSortable(); +$sortable->addSortable(new PicoSort("nama", PicoSort::ORDER_TYPE_ASC)); +$pageable = new PicoPageable(new PicoPage(3, 20)); + +try +{ + $native13 = $obj->native13($pageable, $sortable, true); + echo "\r\nnative13:\r\n"; + foreach($native13 as $sup) + { + echo $sup."\r\n\r\n"; + } +} +catch(Exception $e) +{ + echo $e->getMessage(); +} ``` For the purpose of exporting large amounts of data, use the PDOStatement return type. PDOStatement allows users to read one by one and process it immediately, allowing PHP to release memory from the previous process. PHP does not need to store very large data in a variable. @@ -11987,7 +12089,7 @@ If an operation fails, `PicoSqlite` may throw exceptions or return false. It is ### Conclusion `PicoSqlite` provides an efficient way to interact with SQLite databases. Its straightforward API allows developers to perform common database operations with minimal code. For more advanced database operations, consider extending the class or using additional PDO features. -## Upload File +## File Upload ### Overview @@ -12075,6 +12177,103 @@ else ### Summary This implementation offers a straightforward way to manage file uploads in PHP, abstracting complexities for developers. By using methods like `getAll()` and `isMultiple()`, developers can seamlessly handle both types of uploads without needing to write separate logic for each scenario. This approach not only improves code maintainability but also enhances the developer experience. + +## Resumable File Download + +### Namespace + +`MagicObject\File` + +### Description + +The `PicoDownloadFile` class is designed to facilitate efficient file downloading in PHP, supporting **partial content** (range requests) for large files. It ensures that requested files exist, handles errors gracefully, and enables downloading in chunks to minimize server load and bandwidth consumption, particularly for large files. + +The class supports the following: + +- Verifying the existence of the file. +- Handling byte-range requests for resuming downloads. +- Sending appropriate HTTP headers to manage the download. +- Streaming the file to the client in manageable chunks (default size: 8 KB). +- Returning relevant HTTP status codes and error messages. + +This class is ideal for scenarios where large files need to be served to clients and you want to offer functionality like resuming interrupted downloads. + + +### Constructor + +```php +__construct($filepath, $filename = null) +``` + +**Parameters**: + +- `$filepath` (string): The full path to the file that should be downloaded. +- `$filename` (string|null, optional): The name of the file for download. If not provided, the filename is extracted from the `filepath` using `basename()`. + +**Description**: Initializes the `PicoDownloadFile` object with the path of the file to be downloaded and an optional filename for the download response. If the filename is not specified, the base name of the file is used. + +**Example**: + +```php +$file = new PicoDownloadFile("/path/to/large-file.zip", "downloaded-file.zip"); +``` + +### Method + +```php +download($exit = false) +``` + +**Parameters**: + +- `$exit` (bool, optional): Whether to terminate the script after sending the file. Default is `false`. + +**Returns**: + +- `bool`: Returns `true` if the entire file was successfully sent, `false` if only part of the file was sent (due to range requests). + +**Description**: This method is responsible for initiating the file download process. It performs the following: + +1. Verifies the existence of the file. +2. Handles byte-range requests for partial downloads (useful for resuming interrupted downloads). +3. Sends the appropriate HTTP headers for the file download. +4. Streams the file to the client in chunks of 8 KB (by default). + +If `$exit` is set to `true`, the script will terminate after the file is sent. + +**Example 1** + +```php +download(true); // Initiate download and terminate the script after sending +``` + +**Example 2** + +```php +download(false); // Initiate download without terminate the script after sending +if($finished && file_exists($path)) +{ + unlink($path); // Delete file when finish +} +``` + +### Error Handling + +- **404 - File Not Found**: If the file does not exist at the specified path, a 404 error is returned. +- **416 - Range Not Satisfiable**: If an invalid byte range is requested (e.g., the start byte is larger than the end byte), a 416 error is returned. +- **500 - Internal Server Error**: If there is an issue opening the file for reading (e.g., permissions issues), a 500 error is returned. + + ## Language MagicObject supports multilingual applications. MagicObject allows developers to create entities that support a wide variety of languages that users can choose from. At the same time, different users can use different languages.