I spent a little time looking at this sample code and thinking about the
questions we discussed on the phone yesterday afternoon. I have seen a
number of questions posted to this newsgroup that show that you are not
alone in trying to get started and to get a handle on COM, so I thought
others might benefit from this overview too. This will not subsitute for a
good reading of the COM specification
(http://www.microsoft.com/Com/resources/comdocs.asp) and writing a COMponent
from scratch (no not using ATL COM Appwizard - that does not count, but a
"from scratch component" or perhaps using Don Box's (the COM developer and
instructor who I hold in very high regard) Yet Another COM Libracy (YACL)
http://www.developmentor.com/dbox/yacl.htm). Nor is it a substitute for the
documentation inprovments we need to put in to support the COM capabilities
in PHP. But perhaps it is a start....

COM is a technology which allows the reuse of code written in any language
(by any language) using a standard calling convention and hiding behind APIs
the implementation details (such as what machine the COMponent is stored on
and the executable which houses it). It can be thought of as a super Remote
Procedure Call (RPC) mechanism with some basic Object roots. It separates
Implementation from Interface (I do use C++ and like it a lot, but I dislike
that (unless great care it taken to separate them) the interface and
implementation are bound up in the same header file). The disadvantage of
this is that if I add a new private member to the class devinitions, all
clients must be recompiled and relinked. Certainly it is possible to define
dataless classes (except perhaps a pointer to another class or structure
which holds the data) it is not the "common use" of C++. COM encourages
versioning, separation of implementation from interface and hiding the
implementation details such as executable location and the language it was
written in. Of course there are other technologies with similar goals. In
fact I am not here to advocate COM or to critique it's weeknesses. Nor do I
wish to start a religeous war on the subject. So to clarify - I am not
saying that Corba is bad or that C++ is evil; I am only stating that COM is
well suited to building reusable components (objects) to be glued together
with scripting languages such as PHP, Perl, VB, ... And other technologies
can be used to do the same.

Where to begin? How about we instantiate a component and then analyse the
call and the uses using PHP syntax.

$foo = new COM("MyClass.MyObject");

might (for example) load a DLL into the address space of PHP (In Process),
or communicate with an executable on the same machine (Local) or (load a
proxy stub into the address space of PHP and) result in a DCOM communication
with a Unix host running a version of DCOM for Unix (Remote).

Let's take this call apart.

$foo is a PHP wrapper around a COM IDispatch pointer (IDsipatch is a well
defined Interface which is Implememted by many component builders - i.e.
many components define an implementaion IDispatch). IDispatch iteslf defines
a number of methods:

Perhaps the two most interesting are:
GetIdOfNames(); // Get me a list of all the names, IDs and parameters that
make up all the methods and properties of the object we are implementing
Invoke(); // Given the definition of the methods and properties as described
by GetIdsOfNames(), invoke the numbered function passing the attached
arguments (each as VARIANTs - see definition later) and return any arguments
chanegd by the call.

How does PHP get an IDispatch interface?

The IDispatch pointer is retrieved by a call to CoCreateInstance() or
MkParseDisplayName (COM APIs). The "MyClass.MyObject" string is called a
ProgId. This is a human readable name for a component and is not guaranteed
to be unique in the Universe. i.e. two programmers might pick the same
ProgIds for their components, although in practice I have yet to see this
happen, in theory it could. The COM code in PHP takes this string (which may
be a ProgId (e.g. "Word.Application"), a GUID string (e.g.
{00000010-0000-0010-8000-00AA006D2EA4}) or a Moniker (e.g.
"IIS://LocalHost/w3svc") (Support for the latter two has been added in
4.07). The ProgId or GUID string is mapped to a numeric GUID using
ClsidFromString() (Another COM API) which either builds a GUID from the
string or looks it up in the registry. HKEY_CLASSES_ROOT/<ProgId>/ClassId is
a GUID string (like above) which can be converted into a 140 bit number
which is said to be unique in the universe (a bold claim given the vastness
of the universe). This ClassID is used to find the implementation
HKEY_CLASSES_ROOT/CLSID/<GUIDSTRING>/The resulting GUID is passed to
CoCreateInstance to retrieve the IDispatch pointer. A Moniker is passed to
MkParseDisplayName() which retrieves the IDispatch interface. The GUID
string and ProgId formats are equivalent to the VB CreateObject() call and
the Moniker style is equivalent to the VB GetObject() call.

The result is an IDispatch pointer which may be used to call properties and
methods on the underlying object. This is totally invisible to the PHP coder
as it is wrapped up and returned to be used like any other native PHP object
(e.g. string, or class, or array).

One more point about ProgIds. Often there is a version dependant and a
version independant ProgId. e.g. Word.Application and Word.APplication.9.
Use the former if you just want the object and don't care about version, use
the latter if you need a specific version of an interface.

OK, so what COMponents are out there to use? Most of the office suite have
interfaces. Word.Application, Excel.Application, etc. Microsoft's new ADSI
specification is all COM based. However not all COM is by definition
accessible through the PHP automation interface. IDispatch is not the only
well defined interface in COM (e.g. all objects are derived from IUnknown
which is not directly accessible from PHP). There are other (well defined)
interfaces such as IOleObject and IPersistFile which are required for visual
editing and compound document support in graphical applications. I will
confine this dicussion to IDispatch and the way it pertains to PHP (and
other scripting languages). FOr example the Windows Task Scheduler is
implemented entirely in COM and can only be accessed from C/C++ as it
provides no ITypeLib (that I could fine) or IDispatch interfaces to work
with. I wrote three simple IDispatch derived interfaces to wrap the WTS
capabilities and so provide this interface to PHP and Perl for the project I
am currently developing for my company.

ITypeLib?
Another "well defined" interface which (I think) was designed to replace
IDispatch. The idea was to embed a full description of the Interface, its
methods and properties and any enumerated types it defined into an easy to
use interface. PHP (4.07) supports both ITypeLib and IDispatch interfaces.
Enumerated types in the type library (a resource attached to the
implementation executable) are loaded when the Interface is instantiated,
thus making them available for use in method calls just as they are in VB.
See the example below.

OK so how do I use this IDispatch PHP wrapper returned from new COM()? You
need to know the methods and properties on that object. How do you find
these?

Very likely your Windows box had hundreds of these compoents. Some with
Automation interfaces and some not. If you have the OLE Viewer installed
(http://www.microsoft.com/Com/resources/oleview.asp) it can be used to take
apart the type libraries and show you the interfaces, methods, enumerated
types etc. However this is really no substitute for real documentation. MSDN
documents many of these interfaces (but not all), but it has the unfortunate
bias for Visual Basic and it is not always easy to understand the mappings
between VB and PHP. In particular, PHP does not support named arguments
(although I hear there are discussions of default arguments and named
arguments for the Zend 2.0 engine), nor does it support the following
syntax:

$foo->bar("some string")->baz; # rather you must $tmp = $foo->bar("SOme
String"); $tmp->baz; This has been addressed in the current Zend 2.0 spec (I
understand).

In this example $foo is a PHP COM object, bar() is a method call and baz is
a property.

Further passing arguments by reference requires that and intervining VARIANT
be created.

VARIANT?
A VARIANT is a well defined type in COM to allow any type to be passed in
any argument to a method or returned from a property or method. Thus (under
the hood in the PHP COM code) every string, integer, floating point number,
array, ... must first be mapped to a VARIANT and then passed to the Invoke()
method of the IDispatch interface. A variant is basically a "C" union with a
type designation and either direct data or a pointer to data. Any methods
requiring a pass by reference argument require that you build and pass in a
VARIANT.

$Vrows = new VARIANT(0, VT_I4|VT_BYREF); # Creates a PHP Variant object
which is a reference to a 32 bit ineteger with an initial value of zero.

Let's look at a database example (I am not much of a database guy, but here
goes)

Assume that you have a Microsoft Access database called c:\data\foobar.mdb
(and ODBC Jet drivers and MDAC (Jet and MDAC are available on the Microsoft
Web site if you do not have them))

$DB = new COM("ADODB.Connection"); # Access the ADO COnnection object and
build a COM IDispatch wrapper called $DB
$ConnectString = "DBQ=C:/data/foobar.mdb;DRIVER=Microsoft Access Driver
(*.mdb)"; # I hate using \\ everywhere
$ConnectString = ereg_replace("/", "\\", $ConnectString);
$DB->Open($ConnectString, "sa", ""); # OK we now have access to the database
and can send out queries
$SQLString = "SELECT field1, field2, field3 FROM TheTable WHERE field1='some
condition' AND field3 > \"#20011003#\" ORDER BY field3 DESC";
$Vrows = new VARIANT(0, VT_I4|VT_BYREF);
$RecordSet = $DB->Execute($SQLString, $Vrows);

Hold everything! What is happening here?
I built up a an SQL query and passed it to the ADO COnnection object and it
passed back ANOTHER IDispatch wrapped object called a RecordSet. So now I
have a "collection" (but not a proper COM collection - more later) of
records which match the selection criteria in teh SQL code (assuming I have
a Table named "TheTable" containing fields (field1, field2, and field3 in
the MDB file). The VARIANT is used to retrieve the number of rows affected
by the SQL query - note it is a pass by reference argument to a method.

So we iterate over the RecordSet thus:

while (!$RecordSet->EOF) # EOF is a property, so needs no "()" method call
syntax
{
 $field1 = $RecordSet->Fields("field1");
 $field2 = $RecordSet->Fields("field2");
 $field3 = $RecordSet->Fields("field3"); # Because
$RecordSet->Fields("field3")->Value is not syntactically valid in PHP4

 print "Field1=" . $field1->Value . " Field2=" . $field1->Value . " Field3
=" . $field3->Value . "<BR>";
 $RecordSet->MoveNext(); # Is a method and so $RecordSet->MoveNext; will not
work.
}

$RecordSet->Close();
$DB->Close();

So there you have an OLEDB example that you can try out for yourself. Note
that nowhere in this code did I use com_invoke() or any of the functions
defined in the COM man pages on php.net. I am sure it is possible to access
COM this way (and perhaps there are even times when it is necessary - I hear
that com_set() is needed in some cases) but accessing COM this way has
served me well and I intend to continue to use this syntax.

OK - what about the Word example you sent. Lets look at it again.

<?php
 $word=new COM("Word.Application.9") or die("Cannot start MS Word"); # This
does not work on my machine because I have Ofifce XP (version 10)
 print "Loaded word version ($word->Version)\n";
 $word->visible = 1 ;
 $word->Documents->Add();

 $word->Selection->Typetext("This is a test");
 $word->Selection->Typetext("123");
 $word->Selection->Find->ClearFormatting;
 $word->Selection->Find->Text = "test";       ##<-## # PHP 4.07-dev gets
beyond this for me
 $word->Selection->Find->Forward = False;
 $word->Selection->Find->Wrap = wdFindContinue; # Note the use of an
enumerated constant here.
 $word->Selection->Collapse->Direction=wdCollapseEnd; # Simply fails because
Selection has ne Collapse property
 $word->Selection->Find->Replacement->Text="rest";
 $word->Selection->Find->Execute; # Fails because Execute is a method
(Execute()) but it requires arguments
?>

So let's look at two possible (working) alternatives:
<?php
        $word=new COM("Word.Application") or die("Cannot start MS Word")

        print "Loaded word version ($word->Version)\n";
        $word->visible = 1 ;
        $word->Documents->Add();

        $word->Selection->Typetext("This is a test");
        $word->Selection->Typetext("123");
        $word->Selection->Find->ClearFormatting();
        $word->Selection->Find->Text = "test";
        $word->Selection->Find->Forward = True;
        $word->Selection->Find->Wrap = wdFindContinue;
        $word->Selection->Find->Replacement->ClearFormatting();
        $word->Selection->Find->Replacement->Text="rest";
        $word->Selection->Find->Execute();
        $word->Selection->Typetext("rest");
?>

Here I tried to use the Execute() method, but it only highlighted the word
"test" because there is no way (other than an argument to Execute() to pass
in the wdReplaceAll constant needed to tell it to do the replacement. So I
just cheated and typed over the selection.

<?php
 $word=new COM("Word.Application") or die("Cannot start MS Word");
 print "Loaded word version ($word->Version)\n";
 $word->visible = 1 ;
 $word->Documents->Add();

 $word->Selection->Typetext("This is a test");
 $word->Selection->Typetext(" 123");
 $word->Selection->Find->ClearFormatting();
 $word->Selection->Find->Execute("test", false, false, false, false, false,
true, true, false ,"rest", wdReplaceAll);
?>

In this example, I actually pass all of the replacement arguments in the
Execute call. The MSDN docs for this use named arguments something like:

 with selection
  .find.execute Text:="test", ReplaceText:="rest", Replace:=wdReplaceAll

Since PHP does not (yet) support named arguments (or empty arguments) you
have to call the method filling in all the peices that you might have
allowed to default in VB.

Com COllections? What about ADSI?

Let's tackle these together in a single example. Warning - this is PHP 4.07
and later only. Both Monikers and COM collection aupport are new to 4.07

$Services = new COM("winmgmts:{impersonationLevel=impersonate}") or
die("glump");
$NetworkAdapters =
$Services->InstancesOf('Win32_NetworkAdapterConfiguration',
wbemFlagBidirectional);
# $NetworkAdapters is a COM collection. This means that it implements the
IEnumVARIANT interface
$NetworkAdapterArray = $NetworkAdapters->Next($NetworkAdapters->{'Count'});
# This is one way to grab everything in a collection
# A collection defines methods, Next(), Count(), and Reset()
# Either put it all in an array and foreach it, or ->Next() it one at a time
foreach ($NetworkAdapterArray as $NetworkAdapter)
{
 # $NetworkAdapter is a PHP COM IDispatch wrapper just like we used above.
 $NetworkAdapterSet->Reset;
 echo $NetworkAdapter->{'Caption'} . "<BR>";
 $IpAddressPtr = $NetworkAdapter->IpAddress; # This property returns an
array of IP Addresses bound to the adapter
 $NetMaskPtr = $NetworkAdapter->IpSubnet; # This property returns an array
of IP Masks one each for the IP addresses
 $DefaultGatewayPtr = $NetworkAdapter->DefaultIPGateway;
 $GatewayCostMetricPtr = $NetworkAdapter->GatewayCostMetric;
 if (count($IpAddressPtr) > 0)
 {
  $IpAddress = $IpAddressPtr;
 }
 else
 {
  $IpAddress[0] = "0.0.0.0";
 }
 if (count($NetMaskPtr) > 0)
 {
  $NetMask = $NetMaskPtr;
 }
 else
 {
  $NetMask[0] = "0.0.0.0";
 }
 if (count($DefaultGatewayPtr) > 0)
 {
  $DefaultGateway = $DefaultGatewayPtr;
 }
 else
 {
  $DefaultGateway[0] = "0.0.0.0";
 }
 if (count($GatewayCostMetricPtr) > 0)
 {
  $GatewayCostMetric = $GatewayCostMetricPtr;
 }
 else
 {
  $GatewayCostMetric[0] = 0;
 }

 $count = count($IpAddress);
 for ($ii=0;$ii<$count;$ii++)
 {
  print $NetworkAdapter->ServiceName . (($count > 1) ? ":$ii" : "");
  print "\tLink ecap: HWaddr " . $NetworkAdapter->MACAddress . "\n";
  print "\tinet addr:" . $IpAddress[$ii] . " Mask:" . $NetMask[$ii] . "\n";
  print "\tMetric:$GatewayCostMetric[$ii]\n";
 }
 if ( $NetworkAdapter->DHCPEnabled)
 {
  print "\tDHCP Enabled:\n";
  $Date = $NetworkAdapter->DHCPLeaseObtained;
  $Date = FormatCOMDate($Date);
  print "\tLease Obtained: " . $Date . "\n";
  $Date = $NetworkAdapter->DHCPLeaseExpires;
  $Date = FormatCOMDate($Date);
  print "\tLease Expires: " . $Date . "\n";
  print "\tDHCP Server: " . $NetworkAdapter->DHCPServer . "\n";
 }
 if ($NetworkAdapter->DNSDomain)
 {
  print "Domain: " . $NetworkAdapter->DNSDomain . "\n";
 }
 if ($NetworkAdapter->DNSHostname)
 {
  print "Hostname: " . $NetworkAdapter->DNSHostname . "\n";
 }
 print "\n";
}

I hope this has been useful. It certainly took me longer to write that I had
planned. I have tried to make it as technically accurate as I know how, but
I make no claims about being a COM expert and there may be minor abuses of
the "correct" COM and object phraseology; and of couse my apologies to the
English Language for it terrible abuse in this e-mail.

Alan.

----- Original Message -----
From: "Bob Kaehms" <[EMAIL PROTECTED]>
To: <[EMAIL PROTECTED]>
Sent: Wednesday, October 03, 2001 13:57
Subject: [PHP-WIN] More COM questions...


> I've been playing with COM and PHP for quite a while now.  I have some
> fairly basic scripts working, but can't seem to take advantage of MS Word
> for more than the most basic open/write/save operations.  I've been using
> the VB help and record macro capabilities of word to attempt to get some
> things working in PHP.
>
> I'm using W98, apache 1.3.12, and PHP as a DSO.  Word is Word 2000 (9.0).
>
> Here is a simple example of a script that tries to generate some text, and
then
> do a search and replace to edit a field.
>
> <?php
> $word=new COM("Word.Application.9") or die("Cannot start MS Word");
>     print "Loaded word version ($word->Version)\n";
>     $word->visible = 1 ;
>     $word->Documents->Add();
>
>       $word->Selection->Typetext("This is a test");
>   $word->Selection->Typetext("123");
>   $word->Selection->Find->ClearFormatting;
>   $word->Selection->Find->Text = "test";       ##<-##
>   $word->Selection->Find->Forward = False;
>   $word->Selection->Find->Wrap = wdFindContinue;
>   $word->Selection->Collapse->Direction=wdCollapseEnd;
>   $word->Selection->Find->Replacement->Text="rest";
>   $word->Selection->Find->Execute;
>     ?>
>
> I've created this by first recording a macro, and then trying to convert.
> After commenting out various lines, it seems to hang on ##<-##
> It prints out the first two typetext lines, and then a third "test" at the
> end of the document, in highlighted form (it does not just highlight the
> first instance of "test".
>
> 3 Questions:
>
>   1) Is this revision of OS/MSword/Apache/PHP stable?
>   2) Does the above syntax look correct, or have I missed something?
>   3) Is it possible to pass variables to Word macros via PHP and
>      drive the entire application from the other side?
>
>
> Note that I haven't seemed to have any problems with Excel.
>
>
> Thanks
>
> --Bob
>
>
>
> --
> PHP Windows Mailing List (http://www.php.net/)
> To unsubscribe, e-mail: [EMAIL PROTECTED]
> For additional commands, e-mail: [EMAIL PROTECTED]
> To contact the list administrators, e-mail: [EMAIL PROTECTED]
>
>


-- 
PHP Windows Mailing List (http://www.php.net/)
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]
To contact the list administrators, e-mail: [EMAIL PROTECTED]

Reply via email to