Add new comment

Calling .Net Framework and .Net Assemblies from PHP

Difficulty: 
Come Get Some

In this article you will learn how to consume .Net framework and .Net libraries compiled for any version of the .Net CLR:

  • Use any .Net binaries (even without COM Visibility) and for any version of the .Net framework.
  • Iterate over .Net collections directly from PHP
  • Automatic propagation of .Net errors into native PHP exceptions that can be properly handled
  • Acces native enums and static methods
  • Use class constructors with parameters
  • Debug PHP and .Net code at the same time as if it was a single application.
  • Specify the location of you binaries, so there is no need to register anything in the GAC and you can have version control along with your application.

A piece of code writen in C# like this:

string javascript = "";
Microsoft.Ajax.Utilities.Minifier m = new Microsoft.Ajax.Utilities.Minifier();
Microsoft.Ajax.Utilities.CodeSettings settings = new Microsoft.Ajax.Utilities.CodeSettings();
settings.OutputMode = Microsoft.Ajax.Utilities.OutputMode.SingleLine;
settings.PreserveFunctionNames = false;
string minified = m.MinifyJavaScript(javascript, settings);

Will look like this on PHP:

$minifier = netMinifier::Minifier_Constructor();
$settings = netCodeSettings::CodeSettings_Constructor();
$csssettings = \ms\Microsoft\Ajax\Utilities\netCssSettings::CssSettings_Constructor();
$settings->OutputMode(\ms\Microsoft\Ajax\Utilities\netOutputMode::SingleLine());
$settings->PreserveFunctionNames(FALSE);
$settings->QuoteObjectLiteralProperties(TRUE);
$result = $minifier->MinifyStyleSheet($css, $csssettings, $settings)->Val();

Enabling the COM_DOTNET extension

The first thing you need to do is enable the official Core extension COM_DOTNET that comes bundled with PHP Core. Add this to your php.ini:

[PHP_COM_DOTNET]
extension=php_com_dotnet.dll

To make sure that the extension ir properly enabled and configured use phpinfo():

Registering the NetPhp binary

Next we are going to register the NetPhp COM component. Download and extract the netutilities.dll from here.

Once downloaded place the DLL file somewhere in your system that you will not forget about such as:

D:\netphp\netutilities.dll

Next we are going to use the regasm tool to register the COM component:

"C:\Windows\Microsoft.NET\Framework\v4.0.30319\regasm.exe" d:\netphp\netutilities.dll /codebase

Adding the NetPhp classes to your project

The next step is to add the NetPhp classes to your current or new PHP project. The easiest way to accomplish this is using composer.

Add a composer.json file to the root of your project such as this one:

{
    "require": {
        "drupalonwindows/netphp": "2.x-dev"
    },
}

Run composer install or composer update to retrieve the packages into your project and to generate the required autoload files.

Understading how composer and autoloading works in PHP is crucial to building succesful PHP projects, you can read this from the official documentation for more details.

Preparing the NetPhp runtime

The next step is to tell NetPhp what libraries your are going to use. To do so we instantiate a NetPhpRuntime class and register our assemblies.

// Instantiate and initialize the Model
$runtime = new \NetPhp\Core\NetPhpRuntime('COM', 'netutilities.NetPhpRuntime');
$runtime->Initialize();

// Register the assemblies that we will be using. The runtime has bundles with some
// of the most used assemblies of the .Net framework.
$runtime->RegisterNetFramework2();

// Add both SpreadsheetLight and the OpenXML it depends on.
$runtime->RegisterAssemblyFromFile(APPLICATION_ROOT . '/binaries/SpreadsheetLight.dll', 'SpreadsheetLight');
$runtime->RegisterAssemblyFromFile(APPLICATION_ROOT . '/binaries/DocumentFormat.OpenXml.dll', 'DocumentFormat.OpenXml');
$runtime->RegisterAssemblyFromFile(APPLICATION_ROOT . '/binaries/AjaxMin.dll', 'AjaxMin');

Consuming .Net binaries on the fly

Once you have your runtime you can start operating on .Net types. The first thing you need to ask the runtime for a "Type" representation of the classes you want to instantiate or call static methods or Enums. There are 3 ways of doing so:

// Using the FullName of a type that belongs to an assembly that has already been registered.
$datetime = $runtime->TypeFromName("System.DateTime");

// Using the FullName of a type that has not been registered yet (from a file)
$minifier = $runtime->TypeFromFile("Microsoft.Ajax.Utilities.Minifier", APPLICATION_ROOT . '/binaries/AjaxMin.dll');

// Using the FullName of a type that has not been registered yet (autodiscoverable)
$datetime2 = $runtime->TypeFromAssembly("System.DateTime", "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
  

Instantiating Objects

Objects returned by NetPhp Framework TypeFrom...() calls represent a .Net type. If you really want to instantiate the .Net type create an instance by calling the Instantiate() method:

$datetime->Instantiate();
echo $datetime->ToShortDateString()->Val(); // Outputs 01/01/0001

Calling Instantiate() is like calling the constructor, so you can pass in any parameteres you wish as long as they match any of the constructor overloads:

// We can only use Int32 from native PHP, so parse
// an Int64 that is equivalent to (long) in the DateTime constructor.
$ticks = $runtime->TypeFromName("System.Int64")->Parse('98566569856565656');

$datetime->Instantiate($ticks);
echo $datetime->ToShortDateString()->Val(); // Outputs 07/05/0313

Using Properties

You can access static properties directly on the non instantiated wrappers:

$now = $runtime->TypeFromName("System.DateTime")->Now;
echo $now->ToShortDateString()->Val(); // Outputs "23/10/2015"

$now = $runtime->TypeFromName("System.DateTime")->Now();
echo $now->ToShortDateString()->Val(); // Outputs "23/10/2015"

Notice that you can operate with Properties in two ways: as if they were PHP properties, or by calling a method with their same name. Setting the value of a property is equally easy:

$timer = $runtime->TypeFromName("System.Timers.Timer")->Instantiate();
$timer->AutoReset(TRUE);
$timer->AutoReset = TRUE;

Calling Methods

Calling methods can be done imitating a method call on the PHP object:

$datetime->AddYears(25);

Mixing .Net and PHP types

At any time you can operate with .Net objects using PHP native types, keeping in mind that they will be converted to their .Net siblings. The mappings are as follows:

  • int : System.Int32
  • float: System.Single
  • boolean :. System.Boolean
  • string : System.String

Every call to a method, function, property or any other interaction with the .Net types will return a Wrapper. Call the method Val() on the wrapper to retrieve the native PHP type as long as it is convertible.

// Result here is a .Net string but not a PHP string
$result = $now->ToShortDateString();
// Use VAL to retrieve the internal .Net type
$php_result = $result->Val()

To ease the interaction between .Net and PHP you can call the GetPhpFromJson() method on any Wrapper and what will happen is that .Net will try to serialize the object into JSON, pass it to PHP, and then json_decode it so you can use it:

$data = $timer->GetPhpFromJson();

var_dump($data);

    // Outputs:
    //  object(stdClass)[38]
    //     public 'AutoReset' => boolean true
    //     public 'Enabled' => boolean false
    //     public 'Interval' => int 100
    //     public 'Site' => null
    //     public 'SynchronizingObject' => null
    //     public 'Container' => null

This will allow you to virtually retrieve anything from the .Net realm into PHP. 

Using Enums

Using an enum is as easy as calling Enum() on a Wrapper the represent an Enum .Net type.

In this example we check if today is Monday, the .Net code would be:

var IsMonday = System.DateTime.Now.DayOfWeek == System.DayOfWeek.Monday;

To convert this to PHP we will use this:

$IsMonday = $runtime->TypeFromName("System.DateTime")->Now->DayOfWeek->Equals($runtime->TypeFromName("System.DayOfWeek")->Enum('Monday'));

Notice that we CANNOT directly compare using the PHP equals (==) operator because this will compare our wrappers, not the objects themselves. In exchange, we rely on the Equals() method that is available for any .Net type.

Working with collections

When a wrapper contains an object that has any collection related behaviour (IEnumerable, IList, etc..) on .Net the framework will automatically provide the PHP required wrappers to operate on these collections directly from PHP.

We can create an ArrayList like this:

$list = $runtime->TypeFromName("System.Collections.ArrayList")->Instantiate()->AsIterator();

Notice the call to AsIterator(). You can call this method on any Wrapper and you will enable the collection related functionalities. Of course, this only works if the wrapped object or type on the .Net side implements the required interfaces and methods (Countable, IEnumerable, etc.).

Now add some elements to the ArrayList and output the count:

$list->Add('My first thing');
$list->Add(2562);

echo count($list); // Outputs 2

You can iterate on this object as if it was a PHP array or collection:

foreach ($list as $item) {
 echo "item: {$item->Val()} </br>";
}

// Outputs:
//
// item: My first thing 
// item: 2562 

Consuming .Net binaries with a PHP based class model

One of the key features of NetPhp is the ability go generate a PHP class model that represents the .Net types in your assemblies. 

So instead of dealing with wrappers:

$ticks = $runtime->TypeFromName("System.Int64")->Parse('98566569856565656');

You will be interacting with .Net as if it were a set of PHP classes:

$ticks = \ms\System\netInt64::_Parse('98566569856565656');

By using a PHP class model:

  • You get more readable code
  • Code is less error prompt
  • You get IDE autocomplete
  • You get Type validation for parameters

Details on how to generate and consume a dumped PHP class model for NetPhp can be found here.

Debugging

You can debug at the same time your PHP code and your .Net code as if it was a single application. In the following screenshot you can see 2 instances of Visual Studio (one for PHP and the other one for the C# library), breakpoints work seamlessly as if it was a single application. What you don't get is edit-and-continue or stepping to a previous instruction, but no one has those on PHP anyways.