Oracle #
Oracle Database is the most mature enterprise database management system in the world — used by banks, telecommunications companies, governments, and large organizations that need high reliability and enterprise features like RAC (Real Application Clusters), Flashback, and Advanced Compression. PHP accesses Oracle through two paths: OCI8 (a feature-rich Oracle-specific extension) and PDO_OCI (the PDO driver for Oracle). OCI8 is more recommended for Oracle because it fully supports Oracle-specific features like cursors, LOBs, array binding, and Oracle Cloud connections — things PDO_OCI can’t do perfectly. This article covers both, with emphasis on Oracle features that don’t exist in other databases.
OCI8 vs PDO_OCI #
flowchart TD
PHP[PHP Application] --> A[OCI8\nRecommended for Oracle]
PHP --> B[PDO_OCI\nCode portability]
A --> C[Oracle Call Interface\nOCI Library]
B --> C
C --> D[(Oracle Database\nOn-premise / Cloud)]
style A fill:#dcfce7,stroke:#16a34a
style C fill:#dbeafe| Aspect | OCI8 | PDO_OCI |
|---|---|---|
| Oracle feature support | ✓ Full | Limited |
| Cursors / REF CURSOR | ✓ | ✗ |
| LOBs (CLOB, BLOB) | ✓ Streaming | Limited |
| Array binding | ✓ | ✗ |
| Named parameters | :name | :name |
| Connection pooling | ✓ Built-in DRCP | Manual |
| Code portability | ✗ Oracle only | ✓ |
Use OCI8 for serious Oracle applications — important features like cursors and LOB handling aren’t fully available in PDO_OCI. Use PDO_OCI only when code portability between Oracle and other databases is a priority.
Installing OCI8 #
Prerequisite: Oracle Instant Client #
OCI8 requires the Oracle Instant Client — the Oracle connection library that must be installed separately:
# Linux — Download from: https://www.oracle.com/database/technologies/instant-client/downloads.html
# Choose: instantclient-basiclite-linux.x64-21.x.x.x.zip
# Extract to a directory
sudo mkdir -p /opt/oracle
sudo unzip instantclient-basiclite-linux.x64-21.*.zip -d /opt/oracle/
# Set the library path
echo /opt/oracle/instantclient_21_x | sudo tee /etc/ld.so.conf.d/oracle-instantclient.conf
sudo ldconfig
# Install the OCI8 extension via PECL
sudo apt-get install php8.3-dev php-pear build-essential libaio1
sudo pecl install oci8
# When asked for the Instant Client path:
# instantclient,/opt/oracle/instantclient_21_x
# Enable the extension
echo "extension=oci8.so" | sudo tee /etc/php/8.3/mods-available/oci8.ini
sudo phpenmod oci8
# Verify
php -m | grep oci8
Connecting to Oracle #
Oracle has several ways to define the connection target:
<?php
// 1. Easy Connect — the simplest way
// Format: //host[:port]/service_name
$connection = oci_connect('username', 'password', '//localhost:1521/ORCL');
// 2. Easy Connect with full options
$connection = oci_connect(
username: 'hr',
password: 'welcome1',
connection_string: '//oracle-server.example.com:1521/XEPDB1',
character_set: 'AL32UTF8', // UTF-8 for Unicode
);
// 3. TNS (Transparent Network Substrate) — via tnsnames.ora
// Define an entry in $ORACLE_HOME/network/admin/tnsnames.ora:
// MYDB =
// (DESCRIPTION =
// (ADDRESS = (PROTOCOL = TCP)(HOST = db-server)(PORT = 1521))
// (CONNECT_DATA = (SERVICE_NAME = mydb.example.com))
// )
$connection = oci_connect('hr', 'welcome1', 'MYDB');
// 4. Full connection string (without tnsnames.ora)
$tns = "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))";
$connection = oci_connect('hr', 'welcome1', $tns);
// 5. Oracle Cloud (ATP/ADW) — needs a Wallet
putenv("TNS_ADMIN=/path/to/wallet"); // location of cwallet.sso and tnsnames.ora
$connection = oci_connect('admin', 'MyCloudPassword123!', 'mydb_high');
if ($connection === false) {
$error = oci_error();
throw new \RuntimeException(
"Oracle connection failed: [{$error['code']}] {$error['message']}"
);
}
// Close the connection
oci_close($connection);
// oci_pconnect — persistent connection (reused from the process connection pool)
$persistentConnection = oci_pconnect('hr', 'welcome1', '//localhost/ORCL');
// oci_new_connect — always create a new connection (not from the pool)
$newConnection = oci_new_connect('hr', 'welcome1', '//localhost/ORCL');
Connecting via PDO_OCI #
<?php
// PDO with the Oracle driver
$dsn = 'oci:dbname=//localhost:1521/ORCL;charset=AL32UTF8';
$pdo = new PDO($dsn, 'hr', 'welcome1', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
Prepared Statements and Bind Variables #
Oracle uses the term bind variable for parameters in prepared statements. This isn’t just security (preventing SQL injection) — in Oracle, bind variables also significantly improve performance because a query with bind variables can be parsed once and executed many times from the shared pool.
<?php
// Query with bind variables
$sql = "SELECT id, name, email FROM employees WHERE department_id = :dept AND salary > :min_salary";
$stmt = oci_parse($connection, $sql);
// Bind variables to the statement
oci_bind_by_name($stmt, ':dept', $deptId, -1, SQLT_INT);
oci_bind_by_name($stmt, ':min_salary', $minSalary, -1, SQLT_FLT);
$deptId = 10;
$minSalary = 5000.0;
oci_execute($stmt);
// Fetch the results
while ($row = oci_fetch_assoc($stmt)) {
echo "{$row['ID']}: {$row['NAME']} — {$row['EMAIL']}\n";
// Note: Oracle column names are UPPERCASE by default!
}
oci_free_statement($stmt);
// Fetch everything at once
oci_execute($stmt);
oci_fetch_all($stmt, $results, 0, -1, OCI_FETCHSTATEMENT_BY_ROW + OCI_ASSOC);
print_r($results);
// INSERT with bind variables
$sql = "INSERT INTO products (id, name, price, created_at)
VALUES (product_seq.NEXTVAL, :name, :price, SYSDATE)";
$stmt = oci_parse($connection, $sql);
oci_bind_by_name($stmt, ':name', $name, 100, SQLT_CHR);
oci_bind_by_name($stmt, ':price', $price, -1, SQLT_FLT);
$name = 'Pro Laptop';
$price = 15000000.0;
oci_execute($stmt);
echo oci_num_rows($stmt) . " rows inserted\n";
oci_free_statement($stmt);
Sequences — Auto-increment in Oracle #
Oracle doesn’t have AUTO_INCREMENT — use a Sequence to generate unique sequential values:
-- Create a sequence
CREATE SEQUENCE product_seq
START WITH 1
INCREMENT BY 1
NOCACHE -- or CACHE 20 for performance
NOCYCLE;
-- Use it in an INSERT
INSERT INTO products (id, name) VALUES (product_seq.NEXTVAL, 'Laptop');
-- Get the newly generated value
SELECT product_seq.CURRVAL FROM DUAL;
-- Oracle 12c+: IDENTITY column (like AUTO_INCREMENT)
CREATE TABLE products (
id NUMBER GENERATED ALWAYS AS IDENTITY,
name VARCHAR2(100)
);
<?php
// Insert with a sequence and get the new ID
$sql = "INSERT INTO products (id, name, price)
VALUES (product_seq.NEXTVAL, :name, :price)
RETURNING id INTO :new_id";
$stmt = oci_parse($connection, $sql);
oci_bind_by_name($stmt, ':name', $name, 100);
oci_bind_by_name($stmt, ':price', $price, -1, SQLT_FLT);
oci_bind_by_name($stmt, ':new_id', $newId, -1, SQLT_INT); // output
$name = '4K Monitor';
$price = 5000000.0;
oci_execute($stmt);
echo "New ID: $newId\n"; // value from the sequence
oci_free_statement($stmt);
Cursors and REF CURSOR #
REF CURSOR is a very powerful Oracle feature — it lets stored procedures return result sets that can be iterated from PHP:
<?php
// Stored procedure definition in Oracle (PL/SQL):
/*
CREATE OR REPLACE PROCEDURE get_employees_by_dept(
p_dept_id IN NUMBER,
p_cursor OUT SYS_REFCURSOR
) AS
BEGIN
OPEN p_cursor FOR
SELECT id, name, email, salary
FROM employees
WHERE department_id = p_dept_id
ORDER BY name;
END;
*/
// Call from PHP
$sql = "BEGIN get_employees_by_dept(:dept_id, :cursor); END;";
$stmt = oci_parse($connection, $sql);
$cursor = oci_new_cursor($connection); // create an empty cursor
oci_bind_by_name($stmt, ':dept_id', $deptId, -1, SQLT_INT);
oci_bind_by_name($stmt, ':cursor', $cursor, -1, OCI_B_CURSOR); // bind the cursor
$deptId = 10;
oci_execute($stmt);
oci_execute($cursor); // execute the cursor filled by the stored procedure
// Iterate the cursor results
while ($row = oci_fetch_assoc($cursor)) {
echo "{$row['NAME']}: {$row['EMAIL']}\n";
}
oci_free_statement($cursor);
oci_free_statement($stmt);
PL/SQL Anonymous Blocks #
PHP can execute PL/SQL blocks directly — useful for batch operations or logic better suited to the database:
<?php
// Execute a PL/SQL block
$plsql = "
BEGIN
UPDATE products
SET price = price * (1 + :increase_percent / 100)
WHERE category = :category;
INSERT INTO audit_log (action, detail, created_at)
VALUES ('UPDATE_PRICE', 'Category: ' || :category || ', Increase: ' || :increase_percent || '%', SYSDATE);
COMMIT;
END;
";
$stmt = oci_parse($connection, $plsql);
oci_bind_by_name($stmt, ':increase_percent', $increase, -1, SQLT_FLT);
oci_bind_by_name($stmt, ':category', $category, 50, SQLT_CHR);
$increase = 10.0; // 10% increase
$category = 'ELECTRONICS';
oci_execute($stmt, OCI_NO_AUTO_COMMIT); // without auto-commit
// OCI8 auto-commits by default — OCI_NO_AUTO_COMMIT is for manual transactions
oci_commit($connection); // manual commit
oci_free_statement($stmt);
LOBs — Large Objects (CLOB and BLOB) #
Oracle uses LOB (Large Object) types for large data — CLOB for long text, BLOB for binary:
<?php
// Storing long text (CLOB)
$sql = "INSERT INTO documents (id, title, content) VALUES (doc_seq.NEXTVAL, :title, EMPTY_CLOB())
RETURNING content INTO :clob_var";
$stmt = oci_parse($connection, $sql);
$clob = oci_new_descriptor($connection, OCI_D_LOB); // create a LOB descriptor
oci_bind_by_name($stmt, ':title', $title, 200, SQLT_CHR);
oci_bind_by_name($stmt, ':clob_var', $clob, -1, OCI_B_CLOB);
$title = 'Annual Report 2024';
$content = str_repeat("Very long report content. ", 10000); // large text
oci_execute($stmt, OCI_NO_AUTO_COMMIT);
// Write the content to the LOB
$clob->saveFile = false;
$clob->write($content);
oci_commit($connection);
oci_free_statement($stmt);
$clob->free();
// Reading a CLOB
$stmt = oci_parse($connection, "SELECT content FROM documents WHERE id = :id");
oci_bind_by_name($stmt, ':id', $id, -1, SQLT_INT);
$id = 1;
oci_execute($stmt);
$row = oci_fetch_array($stmt, OCI_ASSOC + OCI_RETURN_LOBS);
$documentContent = $row['CONTENT']; // OCI_RETURN_LOBS automatically loads the LOB into a string
oci_free_statement($stmt);
// Storing a file (BLOB)
$sql = "INSERT INTO files (id, name, data) VALUES (file_seq.NEXTVAL, :name, EMPTY_BLOB())
RETURNING data INTO :blob_var";
$stmt = oci_parse($connection, $sql);
$blob = oci_new_descriptor($connection, OCI_D_LOB);
oci_bind_by_name($stmt, ':name', $fileName, 200, SQLT_CHR);
oci_bind_by_name($stmt, ':blob_var', $blob, -1, OCI_B_BLOB);
$fileName = 'report.pdf';
oci_execute($stmt, OCI_NO_AUTO_COMMIT);
$blob->saveFile('/path/to/report.pdf'); // read from a file and save to the BLOB
oci_commit($connection);
$blob->free();
oci_free_statement($stmt);
Transactions in Oracle #
<?php
// OCI8 auto-commits by default — must be explicit for transactions
function transferFunds(mixed $connection, int $fromId, int $toId, float $amount): void
{
// 1. Reduce the sender's balance (with SELECT FOR UPDATE — row lock)
$stmt = oci_parse($connection, "
SELECT balance FROM accounts WHERE id = :id FOR UPDATE
");
oci_bind_by_name($stmt, ':id', $fromId, -1, SQLT_INT);
oci_execute($stmt, OCI_NO_AUTO_COMMIT); // don't commit yet
$row = oci_fetch_assoc($stmt);
if (!$row || $row['BALANCE'] < $amount) {
oci_rollback($connection);
throw new \DomainException("Insufficient balance");
}
oci_free_statement($stmt);
// 2. Reduce
$stmt = oci_parse($connection, "UPDATE accounts SET balance = balance - :amount WHERE id = :id");
oci_bind_by_name($stmt, ':amount', $amount, -1, SQLT_FLT);
oci_bind_by_name($stmt, ':id', $fromId, -1, SQLT_INT);
oci_execute($stmt, OCI_NO_AUTO_COMMIT);
oci_free_statement($stmt);
// 3. Add to the recipient
$stmt = oci_parse($connection, "UPDATE accounts SET balance = balance + :amount WHERE id = :id");
oci_bind_by_name($stmt, ':amount', $amount, -1, SQLT_FLT);
oci_bind_by_name($stmt, ':id', $toId, -1, SQLT_INT);
oci_execute($stmt, OCI_NO_AUTO_COMMIT);
oci_free_statement($stmt);
// 4. Commit all changes
oci_commit($connection);
}
// Rollback on error
try {
transferFunds($connection, 1, 2, 500000.0);
} catch (\Throwable $e) {
oci_rollback($connection);
throw $e;
}
Important Oracle vs MySQL Differences #
-- 1. Dual table — for queries without a table
SELECT SYSDATE FROM DUAL; -- Oracle
SELECT NOW(); -- MySQL
-- 2. Concatenation
SELECT 'Hello' || ' ' || name FROM employees; -- Oracle
SELECT CONCAT('Hello', ' ', name) FROM users; -- MySQL
-- 3. Strings: single quotes only
SELECT 'text' FROM DUAL; -- Oracle (double quotes are for identifiers)
-- 4. Pagination
-- Oracle 12c+: OFFSET ... FETCH
SELECT * FROM products ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- Oracle 11g: ROWNUM (trickier)
SELECT * FROM (
SELECT p.*, ROWNUM rn FROM (
SELECT * FROM products ORDER BY id
) p WHERE ROWNUM <= 30
) WHERE rn > 20;
-- 5. Uppercase column names by default
-- Oracle returns column names as UPPERCASE
-- Access with uppercase: $row['NAME'], not $row['name']
-- 6. NULL concatenation
SELECT 'a' || NULL FROM DUAL; -- 'a' (NULLs are ignored in Oracle)
SELECT CONCAT('a', NULL); -- NULL (in MySQL)
-- 7. Empty string = NULL in Oracle!
INSERT INTO t (col) VALUES (''); -- NULL in Oracle, not an empty string
Summary #
- OCI8 is the primary choice for Oracle — it supports Oracle-specific features like REF CURSOR, LOB streaming, array binding, and the DRCP connection pool that aren’t fully available in PDO_OCI.
- The Oracle Instant Client must be installed before installing the OCI8 extension — it’s the C library from Oracle that bridges PHP and the database.
- Bind variables in Oracle aren’t just for security — they also improve performance because Oracle caches the query plan in the shared pool for the same bind variables.
- Oracle column names are UPPERCASE by default — access fetch results with
$row['NAME'], not$row['name']. UseAS "name"(quoted) if you want lowercase.- Sequences replace AUTO_INCREMENT — create one with
CREATE SEQUENCE name_seqand useNEXTVALon INSERT. Oracle 12c+ supportsIDENTITYcolumns.- REF CURSOR is how Oracle returns result sets from stored procedures — create one with
oci_new_cursor(), bind it asOCI_B_CURSOR, execute it separately, then fetch.- OCI8 auto-commits by default — use
OCI_NO_AUTO_COMMITinoci_execute()for manual transactions, thenoci_commit()oroci_rollback().- Empty string = NULL in Oracle — unlike MySQL/PostgreSQL. Store genuinely empty strings with a space or change the schema design.